tanjun
A flexible command framework designed to extend Hikari.
Examples
A Tanjun client can be quickly initialised from a Hikari gateway bot through
tanjun.Client.from_gateway_bot, this enables both slash (interaction) and message
command execution:
bot = hikari.GatewayBot("BOT_TOKEN")
# As a note, unless event_managed=False is passed here then this client
# will be managed based on gateway startup and stopping events.
# mention_prefix=True instructs the client to also set mention prefixes on the
# first startup.
client = tanjun.Client.from_gateway_bot(bot, declare_global_commands=True, mention_prefix=True)
component = tanjun.Component()
client.add_component(component)
# Declare a message command with some basic parser logic.
@component.with_command
@tanjun.with_greedy_argument("name", default="World")
@tanjun.as_message_command("test")
async def test_command(ctx: tanjun.abc.Context, name: str) -> None:
await ctx.respond(f"Hello, {name}!")
# Declare a ping slash command
@component.with_command
@tanjun.with_user_slash_option("user", "The user facing command option's description", default=None)
@tanjun.as_slash_command("hello", "The command's user facing description")
async def hello(ctx: tanjun.abc.Context, user: typing.Optional[hikari.User]) -> None:
user = user or ctx.author
await ctx.respond(f"Hello, {user}!")
Alternatively, the client can also be built from a RESTBot but this will only enable slash (interaction) command execution:
bot = hikari.RESTBot("BOT_TOKEN", "Bot")
# declare_global_commands=True instructs the client to set the global commands
# for the relevant bot on first startup (this will replace any previously
# declared commands).
client = tanjun.Client.from_rest_bot(bot, declare_global_commands=True)
# This will load components from modules based on loader functions.
# For more information on this see `tanjun.as_loader`.
client.load_modules("module.paths")
# Note, unlike a gateway bound bot, the rest bot will not automatically start
# itself due to the lack of Hikari lifetime events in this environment and
# will have to be started after the Hikari client.
async def main() -> None:
await bot.start()
async with client.open():
await bot.join()
For more extensive examples see the repository's examples.
View Source
# -*- coding: utf-8 -*- # cython: language_level=3 # BSD 3-Clause License # # Copyright (c) 2020-2022, Faster Speeding # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """A flexible command framework designed to extend Hikari. Examples -------- A Tanjun client can be quickly initialised from a Hikari gateway bot through `tanjun.Client.from_gateway_bot`, this enables both slash (interaction) and message command execution: ```py bot = hikari.GatewayBot("BOT_TOKEN") # As a note, unless event_managed=False is passed here then this client # will be managed based on gateway startup and stopping events. # mention_prefix=True instructs the client to also set mention prefixes on the # first startup. client = tanjun.Client.from_gateway_bot(bot, declare_global_commands=True, mention_prefix=True) component = tanjun.Component() client.add_component(component) # Declare a message command with some basic parser logic. @component.with_command @tanjun.with_greedy_argument("name", default="World") @tanjun.as_message_command("test") async def test_command(ctx: tanjun.abc.Context, name: str) -> None: await ctx.respond(f"Hello, {name}!") # Declare a ping slash command @component.with_command @tanjun.with_user_slash_option("user", "The user facing command option's description", default=None) @tanjun.as_slash_command("hello", "The command's user facing description") async def hello(ctx: tanjun.abc.Context, user: typing.Optional[hikari.User]) -> None: user = user or ctx.author await ctx.respond(f"Hello, {user}!") ``` Alternatively, the client can also be built from a RESTBot but this will only enable slash (interaction) command execution: ```py bot = hikari.RESTBot("BOT_TOKEN", "Bot") # declare_global_commands=True instructs the client to set the global commands # for the relevant bot on first startup (this will replace any previously # declared commands). client = tanjun.Client.from_rest_bot(bot, declare_global_commands=True) # This will load components from modules based on loader functions. # For more information on this see `tanjun.as_loader`. client.load_modules("module.paths") # Note, unlike a gateway bound bot, the rest bot will not automatically start # itself due to the lack of Hikari lifetime events in this environment and # will have to be started after the Hikari client. async def main() -> None: await bot.start() async with client.open(): await bot.join() ``` For more extensive examples see the [repository's examples](https://github.com/FasterSpeeding/Tanjun/tree/master/examples). """ from __future__ import annotations __all__: list[str] = [ # __init__.py "__author__", "__ci__", "__copyright__", "__coverage__", "__docs__", "__email__", "__issue_tracker__", "__license__", "__url__", "__version__", # abc.py "abc", "ClientCallbackNames", # checks.py "checks", "with_all_checks", "with_any_checks", "with_check", "with_dm_check", "with_guild_check", "with_nsfw_check", "with_sfw_check", "with_owner_check", "with_author_permission_check", "with_own_permission_check", # clients.py "clients", "as_loader", "as_unloader", "Client", "MessageAcceptsEnum", # commands.py "commands", "as_message_command", "as_message_command_group", "as_slash_command", "slash_command_group", "MessageCommand", "MessageCommandGroup", "SlashCommand", "SlashCommandGroup", "with_str_slash_option", "with_int_slash_option", "with_float_slash_option", "with_bool_slash_option", "with_role_slash_option", "with_user_slash_option", "with_member_slash_option", "with_channel_slash_option", "with_mentionable_slash_option", # components.py "components", "Component", # context.py "context", # conversion.py "conversion", "to_bool", "to_channel", "to_color", "to_colour", "to_datetime", "to_emoji", "to_guild", "to_invite", "to_invite_with_metadata", "to_member", "to_presence", "to_role", "to_snowflake", "to_user", "to_voice_state", # dependencies.py "dependencies", "BucketResource", "cached_inject", "inject_lc", "InMemoryConcurrencyLimiter", "InMemoryCooldownManager", "LazyConstant", "with_concurrency_limit", "with_cooldown", # errors.py "errors", "CommandError", "ConversionError", "FailedCheck", "FailedModuleLoad", "FailedModuleUnload", "HaltExecution", "MissingDependencyError", "ModuleMissingLoaders", "ModuleStateConflict", "NotEnoughArgumentsError", "TooManyArgumentsError", "ParserError", "TanjunError", # hooks.py "hooks", "AnyHooks", "Hooks", "MessageHooks", "SlashHooks", # injecting.py "injecting", "as_self_injecting", "inject", "injected", # parsing.py "parsing", "ShlexParser", "with_argument", "with_greedy_argument", "with_multi_argument", "with_option", "with_multi_option", "with_parser", # utilities.py "utilities", # repeaters.py "schedules", "as_interval", ] import typing from . import abc from . import context from . import utilities from .abc import ClientCallbackNames from .checks import with_all_checks from .checks import with_any_checks from .checks import with_author_permission_check from .checks import with_check from .checks import with_dm_check from .checks import with_guild_check from .checks import with_nsfw_check from .checks import with_own_permission_check from .checks import with_owner_check from .checks import with_sfw_check from .clients import Client from .clients import MessageAcceptsEnum from .clients import as_loader from .clients import as_unloader from .commands import MessageCommand from .commands import MessageCommandGroup from .commands import SlashCommand from .commands import SlashCommandGroup from .commands import as_message_command from .commands import as_message_command_group from .commands import as_slash_command from .commands import slash_command_group from .commands import with_bool_slash_option from .commands import with_channel_slash_option from .commands import with_float_slash_option from .commands import with_int_slash_option from .commands import with_member_slash_option from .commands import with_mentionable_slash_option from .commands import with_role_slash_option from .commands import with_str_slash_option from .commands import with_user_slash_option from .components import Component from .conversion import to_bool from .conversion import to_channel from .conversion import to_color from .conversion import to_colour from .conversion import to_datetime from .conversion import to_emoji from .conversion import to_guild from .conversion import to_invite from .conversion import to_invite_with_metadata from .conversion import to_member from .conversion import to_presence from .conversion import to_role from .conversion import to_snowflake from .conversion import to_user from .conversion import to_voice_state from .dependencies import BucketResource from .dependencies import InMemoryConcurrencyLimiter from .dependencies import InMemoryCooldownManager from .dependencies import LazyConstant from .dependencies import cached_inject from .dependencies import inject_lc from .dependencies import with_concurrency_limit from .dependencies import with_cooldown from .errors import CommandError from .errors import ConversionError from .errors import FailedCheck from .errors import FailedModuleLoad from .errors import FailedModuleUnload from .errors import HaltExecution from .errors import MissingDependencyError from .errors import ModuleMissingLoaders from .errors import ModuleStateConflict from .errors import NotEnoughArgumentsError from .errors import ParserError from .errors import TanjunError from .errors import TooManyArgumentsError from .hooks import AnyHooks from .hooks import Hooks from .hooks import MessageHooks from .hooks import SlashHooks from .injecting import as_self_injecting from .injecting import inject from .injecting import injected from .parsing import ShlexParser from .parsing import with_argument from .parsing import with_greedy_argument from .parsing import with_multi_argument from .parsing import with_multi_option from .parsing import with_option from .parsing import with_parser from .schedules import as_interval __author__: typing.Final[str] = "Faster Speeding" __ci__: typing.Final[str] = "https://github.com/FasterSpeeding/Tanjun/actions" __copyright__: typing.Final[str] = "© 2020-2021 Faster Speeding" __coverage__: typing.Final[str] = "https://codeclimate.com/github/FasterSpeeding/Tanjun" __docs__: typing.Final[str] = "https://tanjun.cursed.solutions/" __email__: typing.Final[str] = "lucina@lmbyrne.dev" __issue_tracker__: typing.Final[str] = "https://github.com/FasterSpeeding/Tanjun/issues" __license__: typing.Final[str] = "BSD" __url__: typing.Final[str] = "https://github.com/FasterSpeeding/Tanjun" __version__: typing.Final[str] = "2.3.2a1"
View Source
# -*- coding: utf-8 -*- # cython: language_level=3 # BSD 3-Clause License # # Copyright (c) 2020-2022, Faster Speeding # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Interfaces of the objects and clients used within Tanjun.""" from __future__ import annotations __all__: list[str] = [ "ClientLoader", "BaseSlashCommandT", "CommandCallbackSig", "CommandCallbackSigT", "CheckSig", "CheckSigT", "Context", "ClientCallbackNames", "Hooks", "MetaEventSig", "MetaEventSigT", "AnyHooks", "MessageHooks", "SlashHooks", "ExecutableCommand", "HookSig", "HookSigT", "ErrorHookSig", "ErrorHookSigT", "ListenerCallbackSig", "ListenerCallbackSigT", "MaybeAwaitableT", "MessageCommand", "MessageCommandT", "MessageCommandGroup", "MessageContext", "BaseSlashCommand", "SlashCommand", "SlashCommandGroup", "SlashContext", "SlashOption", "Component", "Client", ] import abc import enum import typing from collections import abc as collections import hikari if typing.TYPE_CHECKING: import asyncio import datetime import pathlib from hikari import traits as hikari_traits _T = typing.TypeVar("_T") MaybeAwaitableT = typing.Union[_T, collections.Awaitable[_T]] """Type hint for a value which may need to be awaited to be resolved.""" ContextT = typing.TypeVar("ContextT", bound="Context") ContextT_co = typing.TypeVar("ContextT_co", covariant=True, bound="Context") ContextT_contra = typing.TypeVar("ContextT_contra", bound="Context", contravariant=True) MetaEventSig = collections.Callable[..., MaybeAwaitableT[None]] MetaEventSigT = typing.TypeVar("MetaEventSigT", bound="MetaEventSig") BaseSlashCommandT = typing.TypeVar("BaseSlashCommandT", bound="BaseSlashCommand") MessageCommandT = typing.TypeVar("MessageCommandT", bound="MessageCommand[typing.Any]") CommandCallbackSig = collections.Callable[..., collections.Awaitable[None]] """Type hint of the callback a `Command` instance will operate on. This will be called when executing a command and will need to take at least one positional argument of type `Context` where any other required or optional keyword or positional arguments will be based on the parser instance for the command if applicable. .. note:: This will have to be asynchronous. """ CommandCallbackSigT = typing.TypeVar("CommandCallbackSigT", bound=CommandCallbackSig) """Generic equivalent of `CommandCallbackSig`.""" CheckSig = collections.Callable[..., MaybeAwaitableT[bool]] """Type hint of a general context check used with Tanjun `ExecutableCommand` classes. This may be registered with a `ExecutableCommand` to add a rule which decides whether it should execute for each context passed to it. This should take one positional argument of type `Context` and may either be a synchronous or asynchronous callback which returns `bool` where returning `False` or raising `tanjun.errors.FailedCheck` will indicate that the current context shouldn't lead to an execution. """ CheckSigT = typing.TypeVar("CheckSigT", bound=CheckSig) """Generic equivalent of `CheckSig`""" HookSig = collections.Callable[..., MaybeAwaitableT[None]] """Type hint of the callback used as a general command hook. .. note:: This may be asynchronous or synchronous, dependency injection is supported for this callback's keyword arguments and the positional arguments which are passed dependent on the type of hook this is being registered as. """ HookSigT = typing.TypeVar("HookSigT", bound=HookSig) """Generic equivalent of `HookSig`.""" ErrorHookSig = collections.Callable[..., MaybeAwaitableT[typing.Optional[bool]]] """Type hint of the callback used as a unexpected command error hook. This will be called whenever an unexpected `Exception` is raised during the execution stage of a command (not including expected `tanjun.errors.TanjunError`). This should take two positional arguments - of type `tanjun.abc.Context` and `Exception` - and may be either a synchronous or asynchronous callback which returns `bool` or `None` and may take advantage of dependency injection. `True` is returned to indicate that the exception should be suppressed and `False` is returned to indicate that the exception should be re-raised. """ ErrorHookSigT = typing.TypeVar("ErrorHookSigT", bound=ErrorHookSig) """Generic equivalent of `ErrorHookSig`.""" ListenerCallbackSig = collections.Callable[..., collections.Coroutine[typing.Any, typing.Any, None]] """Type hint of a hikari event manager callback. This is guaranteed one positional arg of type `hikari.Event` regardless of implementation and must be a coruotine function which returns `None`. """ ListenerCallbackSigT = typing.TypeVar("ListenerCallbackSigT", bound=ListenerCallbackSig) """Generic equivalent of `ListenerCallbackSig`.""" class Context(abc.ABC): """Interface for the context of a command execution.""" __slots__ = () @property @abc.abstractmethod def author(self) -> hikari.User: """Object of the user who triggered this command.""" @property @abc.abstractmethod def channel_id(self) -> hikari.Snowflake: """ID of the channel this command was triggered in.""" @property @abc.abstractmethod def cache(self) -> typing.Optional[hikari.api.Cache]: """Hikari cache instance this context's command client was initialised with.""" @property @abc.abstractmethod def client(self) -> Client: """Tanjun `Client` implementation this context was spawned by.""" @property @abc.abstractmethod def component(self) -> typing.Optional[Component]: """Object of the `Component` this context is bound to. .. note:: This will only be `None` before this has been bound to a specific command but never during command execution nor checks. """ @property # TODO: can we somehow have this always be present on the command execution facing interface @abc.abstractmethod def command(self: ContextT) -> typing.Optional[ExecutableCommand[ContextT]]: """Object of the command this context is bound to. .. note:: This will only be `None` before this has been bound to a specific command but never during command execution. """ @property @abc.abstractmethod def created_at(self) -> datetime.datetime: """When this context was created. .. note:: This will either refer to a message or integration's creation date. """ @property @abc.abstractmethod def events(self) -> typing.Optional[hikari.api.EventManager]: """Object of the event manager this context's client was initialised with.""" @property @abc.abstractmethod def guild_id(self) -> typing.Optional[hikari.Snowflake]: """ID of the guild this command was executed in. Will be `None` for all DM command executions. """ @property @abc.abstractmethod def has_responded(self) -> bool: """Whether an initial response has been made for this context.""" @property @abc.abstractmethod def is_human(self) -> bool: """Whether this command execution was triggered by a human. Will be `False` for bot and webhook triggered commands. """ @property @abc.abstractmethod def member(self) -> typing.Optional[hikari.Member]: """Guild member object of this command's author. Will be `None` for DM command executions. """ @property @abc.abstractmethod def server(self) -> typing.Optional[hikari.api.InteractionServer]: """Object of the Hikari interaction server provided for this context's client.""" @property @abc.abstractmethod def rest(self) -> hikari.api.RESTClient: """Object of the Hikari REST client this context's client was initialised with.""" @property @abc.abstractmethod def shards(self) -> typing.Optional[hikari_traits.ShardAware]: """Object of the Hikari shard manager this context's client was initialised with.""" @property def voice(self) -> typing.Optional[hikari.api.VoiceComponent]: """Object of the Hikari voice component this context's client was initialised with.""" @property @abc.abstractmethod def triggering_name(self) -> str: """Command name this execution was triggered with.""" @abc.abstractmethod def set_component(self: _T, _: typing.Optional[Component], /) -> _T: raise NotImplementedError @abc.abstractmethod async def fetch_channel(self) -> hikari.TextableChannel: """Fetch the channel the context was invoked in. .. note:: This performs an API call. Consider using `Context.get_channel` if you have `hikari.config.CacheComponents.GUILD_CHANNELS` cache component enabled. Returns ------- hikari.TextableChannel The textable DM or guild channel the context was invoked in. Raises ------ hikari.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). hikari.ForbiddenError If you are missing the `READ_MESSAGES` permission in the channel. hikari.NotFoundError If the channel is not found. hikari.RateLimitTooLongError Raised in the event that a rate limit occurs that is longer than `max_rate_limit` when making a request. hikari.RateLimitTooLongError Raised in the event that a rate limit occurs that is longer than `max_rate_limit` when making a request. hikari.RateLimitedError Usually, Hikari will handle and retry on hitting rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be detected or handled normally by Hikari due to their undocumented nature, and will trigger this exception if they occur. hikari.InternalServerError If an internal error occurs on Discord while handling the request. """ @abc.abstractmethod async def fetch_guild(self) -> typing.Optional[hikari.Guild]: """Fetch the guild the context was invoked in. .. note:: This performs an API call. Consider using `Context.get_guild` if you have `hikari.config.CacheComponents.GUILDS` cache component enabled. Returns ------- typing.Optional[hikari.Guild] An optional guild the context was invoked in. `None` will be returned if the guild was not found or the context was invoked in a DM channel . Raises ------ hikari.ForbiddenError If you are not part of the guild. hikari.NotFoundError If the guild is not found. hikari.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). hikari.RateLimitTooLongError Raised in the event that a rate limit occurs that is longer than `max_rate_limit` when making a request. hikari.RateLimitedError Usually, Hikari will handle and retry on hitting rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be detected or handled normally by Hikari due to their undocumented nature, and will trigger this exception if they occur. hikari.InternalServerError If an internal error occurs on Discord while handling the request. """ @abc.abstractmethod def get_channel(self) -> typing.Optional[hikari.TextableGuildChannel]: """Retrieve the channel the context was invoked in from the cache. .. note:: This method requires the `hikari.config.CacheComponents.GUILD_CHANNELS` cache component. Returns ------- typing.Optional[hikari.TextableGuildChannel] An optional guild channel the context was invoked in. `None` will be returned if the channel was not found or if it is DM channel. """ @abc.abstractmethod def get_guild(self) -> typing.Optional[hikari.Guild]: """Fetch the guild that the context was invoked in. .. note:: This method requires `hikari.config.CacheComponents.GUILDS` cache component enabled. Returns ------- typing.Optional[hikari.Guild] An optional guild the context was invoked in. `None` will be returned if the guild was not found. """ @abc.abstractmethod async def delete_initial_response(self) -> None: """Delete the initial response after invoking this context. Raises ------ LookupError, hikari.NotFoundError The last context has no initial response. """ @abc.abstractmethod async def delete_last_response(self) -> None: """Delete the last response after invoking this context. Raises ------ LookupError, hikari.NotFoundError The last context has no responses. """ @abc.abstractmethod async def edit_initial_response( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, delete_after: typing.Union[datetime.timedelta, float, int, None] = None, attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED, attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED, component: hikari.UndefinedNoneOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedNoneOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedNoneOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedNoneOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, replace_attachments: bool = False, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, ) -> hikari.Message: """Edit the initial response for this context. Parameters ---------- content : hikari.UndefinedOr[typing.Any] The content to edit the initial response with. If provided, the message contents. If `hikari.UNDEFINED`, then nothing will be sent in the content. Any other value here will be cast to a `str`. If this is a `hikari.Embed` and no `embed` nor `embeds` kwarg is provided, then this will instead update the embed. This allows for simpler syntax when sending an embed alone. Likewise, if this is a `hikari.Resource`, then the content is instead treated as an attachment if no `attachment` and no `attachments` kwargs are provided. Other Parameters ---------------- delete_after : typing.Union[datetime.timedelta, float, int, None] If provided, the seconds after which the response message should be deleted. .. note:: Slash command responses can only be deleted within 14 minutes of the command being received. .. note:: Since (as of writing) ephemeral responses cannot be deleted by the bot, this is ignored for ephemeral slash command responses. attachment : hikari.UndefinedOr[hikari.Resourceish] A singular attachment to edit the initial response with. attachments : hikari.UndefinedOr[collections.abc.Sequence[hikari.Resourceish]] A sequence of attachments to edit the initial response with. component : hikari.UndefinedNoneOr[hikari.api.ComponentBuilder] If provided, builder object of the component to set for this message. This component will replace any previously set components and passing `None` will remove all components. components : hikari.UndefinedNoneOr[collections.abc.Sequence[hikari.api.ComponentBuilder]] If provided, a sequence of the component builder objects set for this message. These components will replace any previously set components and passing `None` or an empty sequence will remove all components. embed : hikari.UndefinedOr[hikari.Embed] An embed to replace the initial response with. embeds : hikari.UndefinedOr[collections.abc.Sequence[hikari.Embed]] A sequence of embeds to replace the initial response with. replace_attachments : bool Whether to replace the attachments of the response or not. Default to `False`. mentions_everyone : hikari.UndefinedOr[bool] If provided, whether the message should parse @everyone/@here mentions. user_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]] If provided, and `True`, all mentions will be parsed. If provided, and `False`, no mentions will be parsed. Alternatively this may be a collection of `hikari.Snowflake`, or `hikari.PartialUser` derivatives to enforce mentioning specific users. role_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]] If provided, and `True`, all mentions will be parsed. If provided, and `False`, no mentions will be parsed. Alternatively this may be a collection of `hikari.Snowflake`, or `hikari.PartialRole` derivatives to enforce mentioning specific roles. Notes ----- Attachments can be passed as many different things, to aid in convenience. * If a `pathlib.PurePath` or `str` to a valid URL, the resource at the given URL will be streamed to Discord when sending the message. Subclasses of `hikari.WebResource` such as `hikari.URL`, `hikari.Attachment`, `hikari.Emoji`, `EmbedResource`, etc will also be uploaded this way. This will use bit-inception, so only a small percentage of the resource will remain in memory at any one time, thus aiding in scalability. * If a `hikari.Bytes` is passed, or a `str` that contains a valid data URI is passed, then this is uploaded with a randomized file name if not provided. * If a `hikari.File`, `pathlib.PurePath` or `str` that is an absolute or relative path to a file on your file system is passed, then this resource is uploaded as an attachment using non-blocking code internally and streamed using bit-inception where possible. This depends on the type of `concurrent.futures.Executor` that is being used for the application (default is a thread pool which supports this behaviour). Returns ------- hikari.Message The message that has been edited. Raises ------ ValueError If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions`. If `delete_after` would be more than 14 minutes after the slash command was called. TypeError If both `attachment` and `attachments` are specified. hikari.BadRequestError This may be raised in several discrete situations, such as messages being empty with no attachments or embeds; messages with more than 2000 characters in them, embeds that exceed one of the many embed limits; too many attachments; attachments that are too large; invalid image URLs in embeds; too many components. hikari.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). hikari.ForbiddenError If you are missing the `SEND_MESSAGES` in the channel or the person you are trying to message has the DM's disabled. hikari.NotFoundError If the channel is not found. hikari.RateLimitTooLongError Raised in the event that a rate limit occurs that is longer than `max_rate_limit` when making a request. hikari.RateLimitedError Usually, Hikari will handle and retry on hitting rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be detected or handled normally by Hikari due to their undocumented nature, and will trigger this exception if they occur. hikari.InternalServerError If an internal error occurs on Discord while handling the request. """ @abc.abstractmethod async def edit_last_response( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, delete_after: typing.Union[datetime.timedelta, float, int, None] = None, attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED, attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED, component: hikari.UndefinedNoneOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedNoneOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedNoneOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedNoneOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, replace_attachments: bool = False, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, ) -> hikari.Message: """Edit the last response for this context. Parameters ---------- content : hikari.UndefinedOr[typing.Any] The content to edit the last response with. If provided, the message contents. If `hikari.UNDEFINED`, then nothing will be sent in the content. Any other value here will be cast to a `str`. If this is a `hikari.Embed` and no `embed` nor `embeds` kwarg is provided, then this will instead update the embed. This allows for simpler syntax when sending an embed alone. Likewise, if this is a `hikari.Resource`, then the content is instead treated as an attachment if no `attachment` and no `attachments` kwargs are provided. Other Parameters ---------------- delete_after : typing.Union[datetime.timedelta, float, int, None] If provided, the seconds after which the response message should be deleted. .. note:: Slash command responses can only be deleted within 14 minutes of the command being received. .. note:: Since (as of writing) ephemeral responses cannot be deleted by the bot, this is ignored for ephemeral slash command responses. attachment : hikari.UndefinedOr[hikari.Resourceish] A singular attachment to edit the last response with. attachments : hikari.UndefinedOr[collections.abc.Sequence[hikari.Resourceish]] A sequence of attachments to edit the last response with. component : hikari.UndefinedNoneOr[hikari.api.ComponentBuilder] If provided, builder object of the component to set for this message. This component will replace any previously set components and passing `None` will remove all components. components : hikari.UndefinedNoneOr[collections.abc.Sequence[hikari.api.ComponentBuilder]] If provided, a sequence of the component builder objects set for this message. These components will replace any previously set components and passing `None` or an empty sequence will remove all components. embed : hikari.UndefinedOr[hikari.Embed] An embed to replace the last response with. embeds : hikari.UndefinedOr[collections.abc.Sequence[hikari.Embed]] A sequence of embeds to replace the last response with. replace_attachments : bool Whether to replace the attachments of the response or not. Default to `False`. mentions_everyone : hikari.UndefinedOr[bool] If provided, whether the message should parse @everyone/@here mentions. user_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]] If provided, and `True`, all mentions will be parsed. If provided, and `False`, no mentions will be parsed. Alternatively this may be a collection of `hikari.Snowflake`, or `hikari.PartialUser` derivatives to enforce mentioning specific users. role_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]] If provided, and `True`, all mentions will be parsed. If provided, and `False`, no mentions will be parsed. Alternatively this may be a collection of `hikari.Snowflake`, or `hikari.PartialRole` derivatives to enforce mentioning specific roles. Notes ----- Attachments can be passed as many different things, to aid in convenience. * If a `pathlib.PurePath` or `str` to a valid URL, the resource at the given URL will be streamed to Discord when sending the message. Subclasses of `hikari.WebResource` such as `hikari.URL`, `hikari.Attachment`, `hikari.Emoji`, `EmbedResource`, etc will also be uploaded this way. This will use bit-inception, so only a small percentage of the resource will remain in memory at any one time, thus aiding in scalability. * If a `hikari.Bytes` is passed, or a `str` that contains a valid data URI is passed, then this is uploaded with a randomized file name if not provided. * If a `hikari.File`, `pathlib.PurePath` or `str` that is an absolute or relative path to a file on your file system is passed, then this resource is uploaded as an attachment using non-blocking code internally and streamed using bit-inception where possible. This depends on the type of `concurrent.futures.Executor` that is being used for the application (default is a thread pool which supports this behaviour). Returns ------- hikari.Message The message that has been edited. Raises ------ ValueError If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions`. If `delete_after` would be more than 14 minutes after the slash command was called. TypeError If both `attachment` and `attachments` are specified. hikari.BadRequestError This may be raised in several discrete situations, such as messages being empty with no attachments or embeds; messages with more than 2000 characters in them, embeds that exceed one of the many embed limits; too many attachments; attachments that are too large; invalid image URLs in embeds; too many components. hikari.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). hikari.ForbiddenError If you are missing the `SEND_MESSAGES` in the channel or the person you are trying to message has the DM's disabled. hikari.NotFoundError If the channel is not found. hikari.RateLimitTooLongError Raised in the event that a rate limit occurs that is longer than `max_rate_limit` when making a request. hikari.RateLimitedError Usually, Hikari will handle and retry on hitting rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be detected or handled normally by Hikari due to their undocumented nature, and will trigger this exception if they occur. hikari.InternalServerError If an internal error occurs on Discord while handling the request. """ @abc.abstractmethod async def fetch_initial_response(self) -> hikari.Message: """Fetch the initial response for this context. Raises ------ LookupError, hikari.NotFoundError The response was not found. """ @abc.abstractmethod async def fetch_last_response(self) -> hikari.Message: """Fetch the last response for this context. Raises ------ LookupError, hikari.NotFoundError The response was not found. """ @typing.overload @abc.abstractmethod async def respond( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, ensure_result: typing.Literal[False] = False, delete_after: typing.Union[datetime.timedelta, float, int, None] = None, component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, ) -> typing.Optional[hikari.Message]: ... @typing.overload @abc.abstractmethod async def respond( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, ensure_result: typing.Literal[True], delete_after: typing.Union[datetime.timedelta, float, int, None] = None, component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, ) -> hikari.Message: ... @abc.abstractmethod async def respond( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, ensure_result: bool = False, delete_after: typing.Union[datetime.timedelta, float, int, None] = None, component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, ) -> typing.Optional[hikari.Message]: """Respond to this context. Parameters ---------- content : hikari.UndefinedOr[typing.Any] The content to respond with. If provided, the message contents. If `hikari.UNDEFINED`, then nothing will be sent in the content. Any other value here will be cast to a `str`. If this is a `hikari.Embed` and no `embed` nor `embeds` kwarg is provided, then this will instead update the embed. This allows for simpler syntax when sending an embed alone. Likewise, if this is a `hikari.Resource`, then the content is instead treated as an attachment if no `attachment` and no `attachments` kwargs are provided. Other Parameters ---------------- ensure_result : bool Ensure that this call will always return a message object. If `True` then this will always return `hikari.Message`, otherwise this will return `Optional[hikari.Message]`. It's worth noting that, under certain scenarios within the slash command flow, this may lead to an extre request being made. delete_after : typing.Union[datetime.timedelta, float, int, None] If provided, the seconds after which the response message should be deleted. .. note:: Slash command responses can only be deleted within 14 minutes of the command being received. .. note:: Since (as of writing) ephemeral responses cannot be deleted by the bot, this is ignored for ephemeral slash command responses. component : hikari.UndefinedOr[hikari.api.ComponentBuilder] If provided, builder object of the component to include in this response. components : hikari.UndefinedOr[collections.abc.Sequence[hikari.api.ComponentBuilder]] If provided, a sequence of the component builder objects to include in this response. embed : hikari.UndefinedOr[hikari.Embed] An embed to respond with. embeds : hikari.UndefinedOr[collections.abc.Sequence[hikari.Embed]] A sequence of embeds to respond with. mentions_everyone : hikari.UndefinedOr[bool] If provided, whether the message should parse @everyone/@here mentions. user_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]] If provided, and `True`, all mentions will be parsed. If provided, and `False`, no mentions will be parsed. Alternatively this may be a collection of `hikari.Snowflake`, or `hikari.PartialUser` derivatives to enforce mentioning specific users. role_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]] If provided, and `True`, all mentions will be parsed. If provided, and `False`, no mentions will be parsed. Alternatively this may be a collection of `hikari.Snowflake`, or `hikari.PartialRole` derivatives to enforce mentioning specific roles. Returns ------- typing.Optional[hikari.Message] The message that has been created if it was immedieatly available or `ensure_result` was set to `True`, else `None`. Raises ------ ValueError If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions`. If `delete_after` would be more than 14 minutes after the slash command was called. TypeError If both `attachment` and `attachments` are specified. hikari.BadRequestError This may be raised in several discrete situations, such as messages being empty with no attachments or embeds; messages with more than 2000 characters in them, embeds that exceed one of the many embed limits; too many attachments; attachments that are too large; invalid image URLs in embeds; too many components. hikari.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). hikari.ForbiddenError If you are missing the `SEND_MESSAGES` in the channel or the person you are trying to message has the DM's disabled. hikari.NotFoundError If the channel is not found. hikari.RateLimitTooLongError Raised in the event that a rate limit occurs that is longer than `max_rate_limit` when making a request. hikari.RateLimitedError Usually, Hikari will handle and retry on hitting rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be detected or handled normally by Hikari due to their undocumented nature, and will trigger this exception if they occur. hikari.InternalServerError If an internal error occurs on Discord while handling the request. """ class MessageContext(Context, abc.ABC): __slots__ = () @property @abc.abstractmethod def command(self) -> typing.Optional[MessageCommand[typing.Any]]: """Command that was invoked. .. note:: This is always set during command, command check and parser converter execution but isn't guaranteed during client callback nor client/component check execution. """ @property @abc.abstractmethod def content(self) -> str: """Content of the context's message minus the triggering name and prefix.""" @property @abc.abstractmethod def message(self) -> hikari.Message: """Message that triggered the context.""" @property @abc.abstractmethod def shard(self) -> typing.Optional[hikari.api.GatewayShard]: """Shard that triggered the context. .. note:: This will be `None` if `ctx.shards` is also `None`. """ @property @abc.abstractmethod def triggering_prefix(self) -> str: """Prefix that triggered the context.""" @property @abc.abstractmethod def triggering_name(self) -> str: """Command name that triggered the context.""" @abc.abstractmethod def set_command(self: _T, _: typing.Optional[MessageCommand[typing.Any]], /) -> _T: raise NotImplementedError @abc.abstractmethod def set_content(self: _T, _: str, /) -> _T: raise NotImplementedError @abc.abstractmethod def set_triggering_name(self: _T, _: str, /) -> _T: raise NotImplementedError @abc.abstractmethod async def respond( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, ensure_result: bool = True, delete_after: typing.Union[datetime.timedelta, float, int, None] = None, attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED, attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED, component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, tts: hikari.UndefinedOr[bool] = hikari.UNDEFINED, nonce: hikari.UndefinedOr[str] = hikari.UNDEFINED, reply: typing.Union[bool, hikari.SnowflakeishOr[hikari.PartialMessage], hikari.UndefinedType] = False, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, mentions_reply: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, ) -> hikari.Message: """Respond to this context. Parameters ---------- content : hikari.UndefinedOr[typing.Any] The content to respond with. If provided, the message contents. If `hikari.UNDEFINED`, then nothing will be sent in the content. Any other value here will be cast to a `str`. If this is a `hikari.Embed` and no `embed` nor `embeds` kwarg is provided, then this will instead update the embed. This allows for simpler syntax when sending an embed alone. Likewise, if this is a `hikari.Resource`, then the content is instead treated as an attachment if no `attachment` and no `attachments` kwargs are provided. Other Parameters ---------------- ensure_result : bool Ensure this method call will return a message object. This does nothing for message command contexts as the result w ill always be immedieatly available. delete_after : typing.Union[datetime.timedelta, float, int, None] If provided, the seconds after which the response message should be deleted. tts : hikari.UndefinedOr[bool] Whether to respond with tts/text to speech or no. reply : typing.Union[bool, hikari.SnowflakeishOr[hikari.PartialMessage], hikari.UndefinedType] Whether to reply instead of sending the content to the context. Defaults to `hikari.UNDEFINED`. Passing `True` here indicates a reply to `MessageContext.message`. nonce : hikari.UndefinedOr[str] The nonce that validates that the message was sent. attachment : hikari.UndefinedOr[hikari.Resourceish] A singular attachment to respond with. attachments : hikari.UndefinedOr[collections.abc.Sequence[hikari.Resourceish]] A sequence of attachments to respond with. component : hikari.UndefinedOr[hikari.api.ComponentBuilder] If provided, builder object of the component to include in this message. components : hikari.UndefinedOr[collections.abc.Sequence[hikari.api.ComponentBuilder]] If provided, a sequence of the component builder objects to include in this message. embed : hikari.UndefinedOr[hikari.Embed] An embed to respond with. embeds : hikari.UndefinedOr[collections.abc.Sequence[hikari.Embed]] A sequence of embeds to respond with. mentions_everyone : hikari.UndefinedOr[bool] If provided, whether the message should parse @everyone/@here mentions. user_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]] If provided, and `True`, all mentions will be parsed. If provided, and `False`, no mentions will be parsed. Alternatively this may be a collection of `hikari.Snowflake`, or `hikari.PartialUser` derivatives to enforce mentioning specific users. role_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]] If provided, and `True`, all mentions will be parsed. If provided, and `False`, no mentions will be parsed. Alternatively this may be a collection of `hikari.Snowflake`, or `hikari.PartialRole` derivatives to enforce mentioning specific roles. Notes ----- Attachments can be passed as many different things, to aid in convenience. * If a `pathlib.PurePath` or `str` to a valid URL, the resource at the given URL will be streamed to Discord when sending the message. Subclasses of `hikari.WebResource` such as `hikari.URL`, `hikari.Attachment`, `hikari.Emoji`, `EmbedResource`, etc will also be uploaded this way. This will use bit-inception, so only a small percentage of the resource will remain in memory at any one time, thus aiding in scalability. * If a `hikari.Bytes` is passed, or a `str` that contains a valid data URI is passed, then this is uploaded with a randomized file name if not provided. * If a `hikari.File`, `pathlib.PurePath` or `str` that is an absolute or relative path to a file on your file system is passed, then this resource is uploaded as an attachment using non-blocking code internally and streamed using bit-inception where possible. This depends on the type of `concurrent.futures.Executor` that is being used for the application (default is a thread pool which supports this behaviour). Returns ------- hikari.Message The message that has been created. Raises ------ ValueError If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions`. If the interaction will have expired before `delete_after` is reached. TypeError If both `attachment` and `attachments` are specified. hikari.BadRequestError This may be raised in several discrete situations, such as messages being empty with no attachments or embeds; messages with more than 2000 characters in them, embeds that exceed one of the many embed limits; too many attachments; attachments that are too large; invalid image URLs in embeds; if `reply` is not found or not in the same channel as `channel`; too many components. hikari.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). hikari.ForbiddenError If you are missing the `SEND_MESSAGES` in the channel or the person you are trying to message has the DM's disabled. hikari.NotFoundError If the channel is not found. hikari.RateLimitTooLongError Raised in the event that a rate limit occurs that is longer than `max_rate_limit` when making a request. hikari.RateLimitedError Usually, Hikari will handle and retry on hitting rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be detected or handled normally by Hikari due to their undocumented nature, and will trigger this exception if they occur. hikari.InternalServerError If an internal error occurs on Discord while handling the request. """ class SlashOption(abc.ABC): """Interface of slash command option with extra logic to help resolve it.""" __slots__ = () @property @abc.abstractmethod def name(self) -> str: """Name of this option.""" @property @abc.abstractmethod def type(self) -> typing.Union[hikari.OptionType, int]: """Type of this option.""" @property @abc.abstractmethod def value(self) -> typing.Union[str, hikari.Snowflake, int, bool, float]: """Value provided for this option. .. note:: For discord entity option types (user, member, channel and role) this will be the entity's ID. """ @abc.abstractmethod def boolean(self) -> bool: """Get the boolean value of this option. Raises ------ TypeError If `SlashOption.type` is not BOOLEAN. """ @abc.abstractmethod def float(self) -> float: """Get the float value of this option. Raises ------ TypeError If `SlashOption.type` is not FLOAT. ValueError If called on the focused option for an autocomplete interaction when it's a malformed (incomplete) float. """ @abc.abstractmethod def integer(self) -> int: """Get the integer value of this option. Raises ------ TypeError If `SlashOption.type` is not INTEGER. ValueError If called on the focused option for an autocomplete interaction when it's a malformed (incomplete) integer. """ @abc.abstractmethod def snowflake(self) -> hikari.Snowflake: """Get the ID of this option. Raises ------ TypeError If `SlashOption.type` is not one of CHANNEL, MENTIONABLE, ROLE or USER. """ @abc.abstractmethod def string(self) -> str: """Get the string value of this option. Raises ------ TypeError If `SlashOption.type` is not STRING. """ @abc.abstractmethod def resolve_value( self, ) -> typing.Union[hikari.InteractionChannel, hikari.InteractionMember, hikari.Role, hikari.User]: """Resolve this option to an object value. Returns ------- typing.Union[hikari.InteractionChannel, hikari.InteractionMember, hikari.Role, hikari.User] The object value of this option. Raises ------ TypeError If the option isn't resolvable. """ @abc.abstractmethod def resolve_to_channel(self) -> hikari.InteractionChannel: """Resolve this option to a channel object. Returns ------- hikari.InteractionChannel The channel object. Raises ------ TypeError If the option is not a channel and a `default` wasn't provided. """ @abc.abstractmethod def resolve_to_member(self, *, default: _T = ...) -> typing.Union[hikari.InteractionMember, _T]: """Resolve this option to a member object. Other Parameters ---------------- default: The default value to return if this option cannot be resolved. If this is not provided, this method will raise a `TypeError` if this option cannot be resolved. Returns ------- typing.Union[hikari.InteractionMember, _T] The member object or `default` if it was provided and this option was a user type but had no member. Raises ------ LookupError If no member was found for this option and a `default` wasn't provided. This includes if the option is a mentionable type which targets a member-less user. This could happen if the user isn't in the current guild or if this command was executed in a DM and this option should still be resolvable to a user. TypeError If the option is not a user option and a `default` wasn't provided. This includes if the option is a mentionable type but doesn't target a user. """ @abc.abstractmethod def resolve_to_mentionable(self) -> typing.Union[hikari.Role, hikari.User, hikari.Member]: """Resolve this option to a mentionable object. Returns ------- typing.Union[hikari.Role, hikari.User, hikari.Member] The mentionable object. Raises ------ TypeError If the option is not a mentionable, user or role type. """ @abc.abstractmethod def resolve_to_role(self) -> hikari.Role: """Resolve this option to a role object. Returns ------- hikari.Role The role object. Raises ------ TypeError If the option is not a role. This includes mentionable options which point towards a user. """ @abc.abstractmethod def resolve_to_user(self) -> typing.Union[hikari.User, hikari.Member]: """Resolve this option to a user object. .. note:: This will resolve to a `hikari.Member` first if the relevant command was executed within a guild and the option targeted one of the guild's members, otherwise it will resolve to `hikari.User`. It's also worth noting that hikari.Member inherits from hikari.User meaning that the return value of this can always be treated as a user. Returns ------- typing.Union[hikari.User, hikari.Member] The user object. Raises ------ TypeError If the option is not a user. This includes mentionable options which point towards a role. """ class SlashContext(Context, abc.ABC): """Interface of a slash command specific context.""" __slots__ = () @property @abc.abstractmethod def command(self) -> typing.Optional[BaseSlashCommand]: """Command that was invoked. .. note:: This should always be set during command, command check execution and command hook execution but isn't guaranteed for client callbacks nor component/client checks. """ @property @abc.abstractmethod def defaults_to_ephemeral(self) -> bool: """Whether the context is marked as defaulting to ephemeral response. This effects calls to `SlashContext.create_followup`, `SlashContext.create_initial_response`, `SlashContext.defer` and `SlashContext.respond` unless the `flags` field is provided for the methods which support it. """ @property @abc.abstractmethod def expires_at(self) -> datetime.datetime: """When this application command context expires. After this time is reached, the message/response methods on this context will always raise `hikari.errors.NotFoundError`. """ @property @abc.abstractmethod def has_been_deferred(self) -> bool: """Whether the initial response for this context has been deferred. .. warning:: If this is `True` when `SlashContext.has_responded` is `False` then `SlashContext.edit_initial_response` will need to be used to create the initial response rather than `SlashContext.create_initial_response`. """ @property @abc.abstractmethod def interaction(self) -> hikari.CommandInteraction: """Interaction this context is for.""" @property @abc.abstractmethod def member(self) -> typing.Optional[hikari.InteractionMember]: """Object of the member that triggered this command if this is in a guild.""" @property @abc.abstractmethod def options(self) -> collections.Mapping[str, SlashOption]: """Mapping of option names to the values provided for them.""" @abc.abstractmethod def set_command(self: _T, _: typing.Optional[BaseSlashCommand], /) -> _T: """Set the command for this context. Parameters ---------- command : typing.Optional[BaseSlashCommand] The command this context is for. """ @abc.abstractmethod def set_ephemeral_default(self: _T, state: bool, /) -> _T: """Set the ephemeral default state for this context. Parameters ---------- state : bool The new ephemeral default state. If this is `True` then all calls to the response creating methods on this context will default to being ephemeral. """ @abc.abstractmethod async def defer( self, flags: typing.Union[hikari.UndefinedType, int, hikari.MessageFlag] = hikari.UNDEFINED ) -> None: """Defer the initial response for this context. .. note:: The ephemeral state of the first response is decided by whether the deferral is ephemeral. Other Parameters ---------------- flags : typing.Union[hikari.UndefinedType, int, hikari.MessageFlag] The flags to use for the initial response. """ @abc.abstractmethod async def mark_not_found(self) -> None: """Mark this context as not found. Dependent on how the client is configured this may lead to a not found response message being sent. """ @abc.abstractmethod async def create_followup( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, delete_after: typing.Union[datetime.timedelta, float, int, None] = None, attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED, attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED, component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, tts: hikari.UndefinedOr[bool] = hikari.UNDEFINED, flags: typing.Union[hikari.UndefinedType, int, hikari.MessageFlag] = hikari.UNDEFINED, ) -> hikari.Message: """Create a followup response for this context. .. warning:: Calling this on a context which hasn't had an initial response yet will lead to a `hikari.NotFoundError` being raised. Parameters ---------- content : hikari.UndefinedOr[typing.Any] If provided, the message contents. If `hikari.UNDEFINED`, then nothing will be sent in the content. Any other value here will be cast to a `str`. If this is a `hikari.Embed` and no `embed` kwarg is provided, then this will instead update the embed. This allows for simpler syntax when sending an embed alone. Likewise, if this is a `hikari.Resource`, then the content is instead treated as an attachment if no `attachment` and no `attachments` kwargs are provided. Other Parameters ---------------- delete_after : typing.Union[datetime.timedelta, float, int, None] If provided, the seconds after which the response message should be deleted. .. note:: Slash command responses can only be deleted within 14 minutes of the command being received. .. note:: Since (as of writing) ephemeral responses cannot be deleted by the bot, this is ignored for ephemeral slash command responses. attachment : hikari.UndefinedOr[hikari.Resourceish] If provided, the message attachment. This can be a resource, or string of a path on your computer or a URL. attachments : hikari.UndefinedOr[collections.abc.Sequence[hikari.Resourceish]] If provided, the message attachments. These can be resources, or strings consisting of paths on your computer or URLs. component : hikari.UndefinedOr[hikari.api.ComponentBuilder] If provided, builder object of the component to include in this message. components : hikari.UndefinedOr[collections.abc.Sequence[hikari.api.ComponentBuilder]] If provided, a sequence of the component builder objects to include in this message. embed : hikari.UndefinedOr[hikari.Embed] If provided, the message embed. embeds : hikari.UndefinedOr[collections.abc.Sequence[hikari.Embed]] If provided, the message embeds. mentions_everyone : hikari.UndefinedOr[bool] If provided, whether the message should parse @everyone/@here mentions. user_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]] If provided, and `True`, all mentions will be parsed. If provided, and `False`, no mentions will be parsed. Alternatively this may be a collection of `hikari.Snowflake`, or `hikari.PartialUser` derivatives to enforce mentioning specific users. role_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]] If provided, and `True`, all mentions will be parsed. If provided, and `False`, no mentions will be parsed. Alternatively this may be a collection of `hikari.Snowflake`, or `hikari.PartialRole` derivatives to enforce mentioning specific roles. tts : hikari.UndefinedOr[bool] If provided, whether the message will be sent as a TTS message. flags : typing.Union[hikari.UndefinedType, int, hikari.MessageFlag] The flags to set for this response. As of writing this can only flag which can be provided is EPHEMERAL, other flags are just ignored. Returns ------- hikari.Message The created message object. Raises ------ hikari.NotFoundError If the current interaction is not found or it hasn't had an initial response yet. hikari.BadRequestError This can be raised if the file is too large; if the embed exceeds the defined limits; if the message content is specified only and empty or greater than `2000` characters; if neither content, file or embeds are specified. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. ValueError If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions. If the interaction will have expired before `delete_after` is reached. TypeError If both `attachment` and `attachments` are specified. """ @abc.abstractmethod async def create_initial_response( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, delete_after: typing.Union[datetime.timedelta, float, int, None] = None, component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, flags: typing.Union[int, hikari.MessageFlag, hikari.UndefinedType] = hikari.UNDEFINED, tts: hikari.UndefinedOr[bool] = hikari.UNDEFINED, ) -> None: """Create the initial response for this context. .. warning:: Calling this on a context which already has an initial response will result in this raising a `hikari.NotFoundError`. This includes if the REST interaction server has already responded to the request and deferrals. Other Parameters ---------------- delete_after : typing.Union[datetime.timedelta, float, int, None] If provided, the seconds after which the response message should be deleted. .. note:: Slash command responses can only be deleted within 14 minutes of the command being received. .. note:: Since (as of writing) ephemeral responses cannot be deleted by the bot, this is ignored for ephemeral slash command responses. content : hikari.UndefinedOr[typing.Any] If provided, the message contents. If `hikari.UNDEFINED`, then nothing will be sent in the content. Any other value here will be cast to a `str`. If this is a `hikari.Embed` and no `embed` nor `embeds` kwarg is provided, then this will instead update the embed. This allows for simpler syntax when sending an embed alone. component : hikari.UndefinedOr[hikari.api.ComponentBuilder] If provided, builder object of the component to include in this message. components : hikari.UndefinedOr[collections.abc.Sequence[hikari.api.ComponentBuilder]] If provided, a sequence of the component builder objects to include in this message. embed : hikari.UndefinedOr[hikari.Embed] If provided, the message embed. embeds : hikari.UndefinedOr[collections.abc.Sequence[hikari.Embed]] If provided, the message embeds. flags : typing.Union[int, hikari.MessageFlag, hikari.UndefinedType] If provided, the message flags this response should have. As of writing the only message flag which can be set here is `hikari.MessageFlag.EPHEMERAL`. tts : hikari.UndefinedOr[bool] If provided, whether the message will be read out by a screen reader using Discord's TTS (text-to-speech) system. mentions_everyone : hikari.UndefinedOr[bool] If provided, whether the message should parse @everyone/@here mentions. user_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool]] If provided, and `True`, all user mentions will be detected. If provided, and `False`, all user mentions will be ignored if appearing in the message body. Alternatively this may be a collection of `hikari.Snowflake`, or `hikari.PartialUser` derivatives to enforce mentioning specific users. role_mentions : hikari.UndefinedOr[typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool]] If provided, and `True`, all role mentions will be detected. If provided, and `False`, all role mentions will be ignored if appearing in the message body. Alternatively this may be a collection of `hikari.Snowflake`, or `hikari.PartialRole` derivatives to enforce mentioning specific roles. Raises ------ ValueError If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions`. If the interaction will have expired before `delete_after` is reached. TypeError If both `embed` and `embeds` are specified. hikari.BadRequestError This may be raised in several discrete situations, such as messages being empty with no embeds; messages with more than 2000 characters in them, embeds that exceed one of the many embed limits; invalid image URLs in embeds. hikari.UnauthorizedError If you are unauthorized to make the request (invalid/missing token). hikari.NotFoundError If the interaction is not found or if the interaction's initial response has already been created. hikari.RateLimitTooLongError Raised in the event that a rate limit occurs that is longer than `max_rate_limit` when making a request. hikari.RateLimitedError Usually, Hikari will handle and retry on hitting rate-limits automatically. This includes most bucket-specific rate-limits and global rate-limits. In some rare edge cases, however, Discord implements other undocumented rules for rate-limiting, such as limits per attribute. These cannot be detected or handled normally by Hikari due to their undocumented nature, and will trigger this exception if they occur. hikari.InternalServerError If an internal error occurs on Discord while handling the request. """ class Hooks(abc.ABC, typing.Generic[ContextT_contra]): """Interface of a collection of callbacks called during set stage of command execution.""" __slots__ = () @abc.abstractmethod def copy(self: _T) -> _T: raise NotImplementedError @abc.abstractmethod def add_on_error(self: _T, callback: ErrorHookSig, /) -> _T: """Add an error callback to this hook object. .. note:: This won't be called for expected `tanjun.TanjunError` derived errors. Parameters ---------- callback : ErrorHookSig The callback to add to this hook. This callback should take two positional arguments (of type `tanjun.abc.ContextT_contra` and `Exception`) and may be either synchronous or asynchronous. Returning `True` indicates that the error should be suppressed, `False` that it should be re-raised and `None` that no decision has been made. This will be accounted for along with the decisions other error hooks make by majority rule. Returns ------- Self The hook object to enable method chaining. """ @abc.abstractmethod def with_on_error(self, callback: ErrorHookSigT, /) -> ErrorHookSigT: """Add an error callback to this hook object through a decorator call. .. note:: This won't be called for expected `tanjun.TanjunError` derived errors. Examples -------- ```py hooks = AnyHooks() @hooks.with_on_error async def on_error(ctx: tanjun.abc.Context, error: Exception) -> bool: if isinstance(error, SomeExpectedType): await ctx.respond("You dun goofed") return True # Indicating that it should be suppressed. await ctx.respond(f"An error occurred: {error}") return False # Indicating that it should be re-raised ``` Parameters ---------- callback : ErrorHookSigT The callback to add to this hook. This callback should take two positional arguments (of type `tanjun.abc.ContextT_contra` and `Exception`) and may be either synchronous or asynchronous. Returning `True` indicates that the error shoul be suppressed, `False` that it should be re-raised and `None` that no decision has been made. This will be accounted for along with the decisions other error hooks make by majority rule. Returns ------- ErrorHookSigT The hook callback which was added. """ @abc.abstractmethod def add_on_parser_error(self: _T, callback: HookSig, /) -> _T: """Add a parser error callback to this hook object. Parameters ---------- callback : HookSig The callback to add to this hook. This callback should take two positional arguments (of type `tanjun.abc.ContextT_contra` and `tanjun.errors.ParserError`), return `None` and may be either synchronous or asynchronous. It's worth noting that this unlike general error handlers, this will always suppress the error. Returns ------- Self The hook object to enable method chaining. """ @abc.abstractmethod def with_on_parser_error(self, callback: HookSigT, /) -> HookSigT: """Add a parser error callback to this hook object through a decorator call. Examples -------- ```py hooks = AnyHooks() @hooks.with_on_parser_error async def on_parser_error(ctx: tanjun.abc.Context, error: tanjun.errors.ParserError) -> None: await ctx.respond(f"You gave invalid input: {error}") ``` Parameters ---------- callback : HookSigT The parser error callback to add to this hook. This callback should take two positional arguments (of type `tanjun.abc.ContextT_contra` and `tanjun.errors.ParserError`), return `None` and may be either synchronous or asynchronous. Returns ------- HookSigT The callback which was added. """ @abc.abstractmethod def add_post_execution(self: _T, callback: HookSig, /) -> _T: """Add a post-execution callback to this hook object. Parameters ---------- callback : HookSig The callback to add to this hook. This callback should take one positional argument (of type `tanjun.abc.ContextT_contra`), return `None` and may be either synchronous or asynchronous. Returns ------- Self The hook object to enable method chaining. """ @abc.abstractmethod def with_post_execution(self, callback: HookSigT, /) -> HookSigT: """Add a post-execution callback to this hook object through a decorator call. Examples -------- ```py hooks = AnyHooks() @hooks.with_post_execution async def post_execution(ctx: tanjun.abc.Context) -> None: await ctx.respond("You did something") ``` Parameters ---------- callback : HookSigT The post-execution callback to add to this hook. This callback should take one positional argument (of type `tanjun.abc.ContextT_contra`), return `None` and may be either synchronous or asynchronous. Returns ------- HookSigT The post-execution callback which was seaddedt. """ @abc.abstractmethod def add_pre_execution(self: _T, callback: HookSig, /) -> _T: """Add a pre-execution callback for this hook object. Parameters ---------- callback : HookSig The callback to add to this hook. This callback should take one positional argument (of type `tanjun.abc.ContextT_contra`), return `None` and may be either synchronous or asynchronous. Returns ------- Self The hook object to enable method chaining. """ @abc.abstractmethod def with_pre_execution(self, callback: HookSigT, /) -> HookSigT: """Add a pre-execution callback to this hook object through a decorator call. Examples -------- ```py hooks = AnyHooks() @hooks.with_pre_execution async def pre_execution(ctx: tanjun.abc.Context) -> None: await ctx.respond("You did something") ``` Parameters ---------- callback : HookSigT The pre-execution callback to add to this hook. This callback should take one positional argument (of type `tanjun.abc.ContextT_contra`), return `None` and may be either synchronous or asynchronous. Returns ------- HookSigT The pre-execution callback which was added. """ @abc.abstractmethod def add_on_success(self: _T, callback: HookSig, /) -> _T: """Add a success callback to this hook object. Parameters ---------- callback : HookSig The callback to add to this hook. This callback should take one positional argument (of type `tanjun.abc.ContextT_contra`), return `None` and may be either synchronous or asynchronous. Returns ------- Self The hook object to enable method chaining. """ @abc.abstractmethod def with_on_success(self, callback: HookSigT, /) -> HookSigT: """Add a success callback to this hook object through a decorator call. Examples -------- ```py hooks = AnyHooks() @hooks.with_on_success async def on_success(ctx: tanjun.abc.Context) -> None: await ctx.respond("You did something") ``` Parameters ---------- callback : HookSigT The success callback to add to this hook. This callback should take one positional argument (of type `tanjun.abc.ContextT_contra`), return `None` and may be either synchronous or asynchronous. Returns ------- HookSigT The success callback which was added. """ @abc.abstractmethod async def trigger_error( self, ctx: ContextT_contra, /, exception: Exception, *, hooks: typing.Optional[collections.Set[Hooks[ContextT_contra]]] = None, ) -> int: raise NotImplementedError @abc.abstractmethod async def trigger_post_execution( self, ctx: ContextT_contra, /, *, hooks: typing.Optional[collections.Set[Hooks[ContextT_contra]]] = None, ) -> None: raise NotImplementedError @abc.abstractmethod async def trigger_pre_execution( self, ctx: ContextT_contra, /, *, hooks: typing.Optional[collections.Set[Hooks[ContextT_contra]]] = None, ) -> None: raise NotImplementedError @abc.abstractmethod async def trigger_success( self, ctx: ContextT_contra, /, *, hooks: typing.Optional[collections.Set[Hooks[ContextT_contra]]] = None, ) -> None: raise NotImplementedError AnyHooks = Hooks[Context] """Execution hooks for any context.""" MessageHooks = Hooks[MessageContext] """Execution hooks for messages commands.""" SlashHooks = Hooks[SlashContext] """Execution hooks for slash commands.""" class ExecutableCommand(abc.ABC, typing.Generic[ContextT_co]): """Base class for all commands that can be executed.""" __slots__ = () @property @abc.abstractmethod def checks(self) -> collections.Collection[CheckSig]: """Collection of checks that must be met before the command can be executed.""" @property @abc.abstractmethod def component(self) -> typing.Optional[Component]: """Component that the command is registered with.""" @property @abc.abstractmethod def hooks(self) -> typing.Optional[Hooks[ContextT_co]]: """Hooks that are triggered when the command is executed.""" @property @abc.abstractmethod def metadata(self) -> collections.MutableMapping[typing.Any, typing.Any]: """Mutable mapping of metadata set for this command. .. note:: Any modifications made to this mutable mapping will be preserved by the command. """ @abc.abstractmethod def bind_client(self: _T, client: Client, /) -> _T: raise NotImplementedError @abc.abstractmethod def bind_component(self: _T, component: Component, /) -> _T: raise NotImplementedError @abc.abstractmethod def copy(self: _T) -> _T: """Create a copy of this command. Returns ------- Self A copy of this command. """ @abc.abstractmethod def set_hooks(self: _T, _: typing.Optional[Hooks[ContextT_co]], /) -> _T: """Set the hooks that are triggered when the command is executed. Parameters ---------- hooks : typing.Optional[Hooks[ContextT_co]] The hooks that are triggered when the command is executed. Returns ------- Self This command to enable chained calls """ @abc.abstractmethod def add_check(self: _T, check: CheckSig, /) -> _T: # TODO: remove or add with_check? """Add a check to the command. Parameters ---------- check : CheckSig The check to add. Returns ------- Self This command to enable chained calls """ @abc.abstractmethod def remove_check(self: _T, check: CheckSig, /) -> _T: """Remove a check from the command. Parameters ---------- check : CheckSig The check to remove. Raises ------ ValueError If the provided check isn't found. Returns ------- Self This command to enable chained calls """ @abc.abstractmethod def set_metadata(self: _T, key: typing.Any, value: typing.Any, /) -> _T: """Set a field in the command's metadata. Parameters ---------- key : typing.Any Metadata key to set. value : typing.Any Metadata value to set. Returns ------- Self The command instance to enable chained calls. """ class BaseSlashCommand(ExecutableCommand[SlashContext], abc.ABC): """Base class for all slash command classes.""" __slots__ = () @property @abc.abstractmethod def defaults_to_ephemeral(self) -> typing.Optional[bool]: """Whether contexts executed by this command should default to ephemeral responses. This effects calls to `SlashContext.create_followup`, `SlashContext.create_initial_response`, `SlashContext.defer` and `SlashContext.respond` unless the `flags` field is provided for the methods which support it. Returns ------- bool Whether calls to this command should default to ephemeral mode. If this is `None` then the default from the parent command(s), component or client is used. """ @property @abc.abstractmethod def is_global(self) -> bool: """Whether the command should be declared globally or not. .. warning:: For commands within command groups the state of this flag is inherited regardless of what it's set as on the child command. """ @property @abc.abstractmethod def name(self) -> str: """Name of the command.""" @property @abc.abstractmethod def parent(self) -> typing.Optional[SlashCommandGroup]: """Object of the group this command is in.""" @property def tracked_command(self) -> typing.Optional[hikari.Command]: """Object of the actual command this object tracks if set.""" @property @abc.abstractmethod def tracked_command_id(self) -> typing.Optional[hikari.Snowflake]: """ID of the actual command this object tracks if set.""" @abc.abstractmethod def build(self) -> hikari.api.CommandBuilder: """Get a builder object for this command. Returns ------- hikari.api.CommandBuilder A builder object for this command. Use to declare this command on globally or for a specific guild. """ @abc.abstractmethod async def check_context(self, ctx: SlashContext, /) -> bool: raise NotImplementedError @abc.abstractmethod async def execute( self, ctx: SlashContext, /, option: typing.Optional[hikari.CommandInteractionOption] = None, *, hooks: typing.Optional[collections.MutableSet[SlashHooks]] = None, ) -> None: raise NotImplementedError @abc.abstractmethod def set_parent(self: _T, _: typing.Optional[SlashCommandGroup], /) -> _T: raise NotImplementedError @abc.abstractmethod def set_tracked_command(self: _T, command: hikari.Command, /) -> _T: """Set the global command this tracks. Parameters ---------- command : hikari.Command Object of the global command this tracks. Returns ------- Self The command instance to enable chained calls. """ class SlashCommand(BaseSlashCommand, abc.ABC, typing.Generic[CommandCallbackSigT]): """A command that can be executed in a slash context.""" __slots__ = () @property @abc.abstractmethod def callback(self) -> CommandCallbackSigT: """Callback which is called during execution.""" class SlashCommandGroup(BaseSlashCommand, abc.ABC): """Standard interface of a slash command group. .. note:: Unlike `MessageCommandGroup`, slash command groups do not have their own callback. """ __slots__ = () @property @abc.abstractmethod def commands(self) -> collections.Collection[BaseSlashCommand]: """Collection of the commands in this group.""" @abc.abstractmethod def add_command(self: _T, command: BaseSlashCommand, /) -> _T: """Add a command to this group. Parameters ---------- command : BaseSlashCommand The command to add. Returns ------- Self The command group instance to enable chained calls. """ @abc.abstractmethod def remove_command(self: _T, command: BaseSlashCommand, /) -> _T: """Remove a command from this group. Parameters ---------- command : BaseSlashCommand The command to remove. Raises ------ ValueError If the provided command isn't found. Returns ------- Self The command group instance to enable chained calls. """ @abc.abstractmethod def with_command(self, command: BaseSlashCommandT, /) -> BaseSlashCommandT: """Add a command to this group through a decorator call. Parameters ---------- command : BaseSlashCommand The command to add. Returns ------- BaseSlashCommand The added command. """ class MessageParser(abc.ABC): """Base class for a message parser.""" __slots__ = () @abc.abstractmethod def bind_client(self: _T, client: Client, /) -> _T: raise NotImplementedError @abc.abstractmethod def bind_component(self: _T, component: Component, /) -> _T: raise NotImplementedError @abc.abstractmethod def copy(self: _T) -> _T: """Copy the parser. Returns ------- Self A copy of the parser. """ @abc.abstractmethod async def parse(self, ctx: MessageContext, /) -> dict[str, typing.Any]: """Parse a message context. .. warning:: This relies on the prefix and command name(s) having been removed from `tanjun.abc.MessageContext.content` Parameters ---------- ctx : tanjun.abc.MessageContext The message context to parse. Returns ------- dict[str, typing.Any] Dictionary of argument names to the parsed values for them. Raises ------ tanjun.errors.ParserError If the message could not be parsed. """ class MessageCommand(ExecutableCommand[MessageContext], abc.ABC, typing.Generic[CommandCallbackSigT]): """Standard interface of a message command.""" __slots__ = () @property @abc.abstractmethod def callback(self) -> CommandCallbackSigT: """Callback which is called during execution. .. note:: For command groups, this is called when none of the inner-commands matches the message. """ @property @abc.abstractmethod def names(self) -> collections.Collection[str]: """Collection of this command's names.""" @property @abc.abstractmethod def parent(self) -> typing.Optional[MessageCommandGroup[typing.Any]]: """Parent group of this command if applicable.""" @property @abc.abstractmethod def parser(self) -> typing.Optional[MessageParser]: """Parser for this command.""" @abc.abstractmethod def set_parent(self: _T, _: typing.Optional[MessageCommandGroup[typing.Any]], /) -> _T: """Set the parent of this command. Parameters ---------- parent : typing.Optional[MessageCommandGroup[typing.Any]] The parent of this command. Returns ------- Self The command instance to enable chained calls. """ @abc.abstractmethod def set_parser(self: _T, _: MessageParser, /) -> _T: """Set the for this message command. Parameters ---------- parser : MessageParser The parser to set. Returns ------- Self The command instance to enable chained calls. """ @abc.abstractmethod def copy(self: _T, *, parent: typing.Optional[MessageCommandGroup[typing.Any]] = None) -> _T: """Create a copy of this command. Other Parameters ---------------- parent : typing.Optional[MessageCommandGroup[tping.Any]] The parent of the copy. Returns ------- Self The copy. """ @abc.abstractmethod async def check_context(self, ctx: MessageContext, /) -> bool: raise NotImplementedError @abc.abstractmethod async def execute( self, ctx: MessageContext, /, *, hooks: typing.Optional[collections.MutableSet[Hooks[MessageContext]]] = None ) -> None: raise NotImplementedError class MessageCommandGroup(MessageCommand[CommandCallbackSigT], abc.ABC): """Standard interface of a message command group.""" __slots__ = () @property @abc.abstractmethod def commands(self) -> collections.Collection[MessageCommand[typing.Any]]: """Collection of the commands in this group. .. note:: This may include command groups. """ @abc.abstractmethod def add_command(self: _T, command: MessageCommand[typing.Any], /) -> _T: """Add a command to this group. Parameters ---------- command : MessageCommand The command to add. Returns ------- Self The group instance to enable chained calls. """ @abc.abstractmethod def remove_command(self: _T, command: MessageCommand[typing.Any], /) -> _T: """Remove a command from this group. Parameters ---------- command : MessageCommand The command to remove. Raises ------ ValueError If the provided command isn't found. Returns ------- Self The group instance to enable chained calls. """ @abc.abstractmethod def with_command(self, command: MessageCommandT, /) -> MessageCommandT: """Add a command to this group through a decorator call. Parameters ---------- command : MessageCommand The command to add. Returns ------- MessageCommand The added command. """ class Component(abc.ABC): """Standard interface of a Tanjun component. This is a collection of message and slash commands, and listeners with logic for command search + execution and loading the listeners into a tanjun client. """ __slots__ = () @property @abc.abstractmethod def client(self) -> typing.Optional[Client]: """Tanjun client this component is bound to.""" @property @abc.abstractmethod def defaults_to_ephemeral(self) -> typing.Optional[bool]: """Whether slash contexts executed in this component should default to ephemeral responses. This effects calls to `SlashContext.create_followup`, `SlashContext.create_initial_response`, `SlashContext.defer` and `SlashContext.respond` unless the `flags` field is provided for the methods which support it. Notes ----- * This may be overridden by `BaseSlashCommand.defaults_to_ephemeral`. * This only effects slash command execution. * If this is `None` then the default from the parent client is used. """ @property @abc.abstractmethod def loop(self) -> typing.Optional[asyncio.AbstractEventLoop]: """The asyncio loop this client is bound to if it has been opened.""" @property @abc.abstractmethod def name(self) -> str: """Component's unique identifier. .. note:: This will be preserved between copies of a component. """ @property @abc.abstractmethod def slash_commands(self) -> collections.Collection[BaseSlashCommand]: """Collection of the slash commands in this component.""" @property @abc.abstractmethod def message_commands(self) -> collections.Collection[MessageCommand[typing.Any]]: """Collection of the message commands in this component.""" @property @abc.abstractmethod def listeners(self) -> collections.Mapping[type[hikari.Event], collections.Collection[ListenerCallbackSig]]: """Mapping of event types to the listeners registered for them in this component.""" @property @abc.abstractmethod def metadata(self) -> collections.MutableMapping[typing.Any, typing.Any]: """Mutable mapping of the metadata set for this component. .. note:: Any modifications made to this mutable mapping will be preserved by the component. """ @abc.abstractmethod def set_metadata(self: _T, key: typing.Any, value: typing.Any, /) -> _T: """Set a field in the component's metadata. Parameters ---------- key : typing.Any Metadata key to set. value : typing.Any Metadata value to set. Returns ------- Self The component instance to enable chained calls. """ @abc.abstractmethod def add_slash_command(self: _T, command: BaseSlashCommand, /) -> _T: """Add a slash command to this component. Parameters ---------- command : BaseSlashCommand The command to add. Returns ------- Self The component to enable chained calls. """ @abc.abstractmethod def remove_slash_command(self: _T, command: BaseSlashCommand, /) -> _T: """Remove a slash command from this component. Parameters ---------- command : BaseSlashCommand The command to remove. Raises ------ ValueError If the provided command isn't found. Returns ------- Self The component to enable chained calls. """ @typing.overload @abc.abstractmethod def with_slash_command(self, command: BaseSlashCommandT, /) -> BaseSlashCommandT: ... @typing.overload @abc.abstractmethod def with_slash_command( self, /, *, copy: bool = False ) -> collections.Callable[[BaseSlashCommandT], BaseSlashCommandT]: ... @abc.abstractmethod def with_slash_command( self, command: BaseSlashCommandT = ..., /, *, copy: bool = False ) -> typing.Union[BaseSlashCommandT, collections.Callable[[BaseSlashCommandT], BaseSlashCommandT]]: """Add a slash command to this component through a decorator call. Parameters ---------- command : BaseSlashCommandT The command to add. Other Parameters ---------------- copy : bool Whether to copy the command before adding it. Returns ------- BaseSlashCommandT The added command. """ @abc.abstractmethod def add_message_command(self: _T, command: MessageCommand[typing.Any], /) -> _T: """Add a message command to this component. Parameters ---------- command : MessageCommand[typing.Any] The command to add. Returns ------- Self The component to enable chained calls. """ @abc.abstractmethod def remove_message_command(self: _T, command: MessageCommand[typing.Any], /) -> _T: """Remove a message command from this component. Parameters ---------- command : MessageCommand[typing.Any] The command to remove. Raises ------ ValueError If the provided command isn't found. Returns ------- Self The component to enable chained calls. """ @typing.overload @abc.abstractmethod def with_message_command(self, command: MessageCommandT, /) -> MessageCommandT: ... @typing.overload @abc.abstractmethod def with_message_command( self, /, *, copy: bool = False ) -> collections.Callable[[MessageCommandT], MessageCommandT]: ... @abc.abstractmethod def with_message_command( self, command: MessageCommandT = ..., /, *, copy: bool = False ) -> typing.Union[MessageCommandT, collections.Callable[[MessageCommandT], MessageCommandT]]: """Add a message command to this component through a decorator call. Parameters ---------- command : MessageCommandT The command to add. Other Parameters ---------------- copy : bool Whether to copy the command before adding it. Returns ------- MessageCommandT The added command. """ @abc.abstractmethod def add_listener(self: _T, event: type[hikari.Event], listener: ListenerCallbackSig, /) -> _T: """Add a listener to this component. Parameters ---------- event : type[hikari.Event] The event to listen for. listener : ListenerCallbackSig The listener to add. Returns ------- Self The component to enable chained calls. """ @abc.abstractmethod def remove_listener(self: _T, event: type[hikari.Event], listener: ListenerCallbackSig, /) -> _T: """Remove a listener from this component. Parameters ---------- event : type[hikari.Event] The event to listen for. listener : ListenerCallbackSig The listener to remove. Raises ------ ValueError If the listener is not registered for the provided event. Returns ------- Self The component to enable chained calls. """ # TODO: make event optional? @abc.abstractmethod def with_listener( self, event_type: type[hikari.Event] ) -> collections.Callable[[ListenerCallbackSigT], ListenerCallbackSigT,]: """Add a listener to this component through a decorator call. Parameters ---------- event_type : type[hikari.Event] The event to listen for. Returns ------- collections.abc.Callable[[ListenerCallbackSigT], ListenerCallbackSigT] Decorator callback which takes listener to add. """ @abc.abstractmethod def bind_client(self: _T, client: Client, /) -> _T: raise NotImplementedError @abc.abstractmethod def unbind_client(self: _T, client: Client, /) -> _T: raise NotImplementedError @abc.abstractmethod def check_message_name(self, name: str, /) -> collections.Iterator[tuple[str, MessageCommand[typing.Any]]]: """Check whether a name matches any of this component's registered message commands. Notes ----- * This only checks for name matches against the top level command and will not account for sub-commands. * Dependent on implementation detail this may partial check name against command names using name.startswith(command_name), hence why it also returns the name a command was matched by. Parameters ---------- name : str The name to check for command matches. Returns ------- collections.abc.Iterator[tuple[str, MessageCommand[typing.Any]]] Iterator of tuples of command name matches to the relevant message command objects. """ @abc.abstractmethod def check_slash_name(self, name: str, /) -> collections.Iterator[BaseSlashCommand]: """Check whether a name matches any of this component's registered slash commands. .. note:: This won't check for sub-commands and will expect `name` to simply be the top level command name. Parameters ---------- name : str The name to check for command matches. Returns ------- collections.abc.Iterator[BaseSlashCommand] An iterator of the matching slash commands. """ @abc.abstractmethod async def execute_interaction( self, ctx: SlashContext, /, *, hooks: typing.Optional[collections.MutableSet[SlashHooks]] = None, ) -> typing.Optional[collections.Awaitable[None]]: """Execute a slash context. .. note:: Unlike `Component.execute_message`, this shouldn't be expected to raise `tanjun.errors.HaltExecution` nor `tanjun.errors.CommandError`. Parameters ---------- ctx : SlashContext The context to execute. Other Parameters ---------------- hooks : typing.Optional[collections.abc.MutableSet[SlashHooks]] = None Set of hooks to include in this command execution. Returns ------- typing.Optional[collections.abc.Awaitable[None]] Awaitable used to wait for the command execution to finish. This may be awaited or left to run as a background task. If this is `None` then the client should carry on its search for a component with a matching command. """ @abc.abstractmethod async def execute_message( self, ctx: MessageContext, /, *, hooks: typing.Optional[collections.MutableSet[MessageHooks]] = None ) -> bool: """Execute a message context. Parameters ---------- ctx : MessageContext The context to execute. Other Parameters ---------------- hooks : typing.Optional[collections.abc.MutableSet[MessageHooks]] = None Set of hooks to include in this command execution. Returns ------- bool Whether a message command was executed in this component with the provided context. If `False` then the client should carry on its search for a component with a matching command. Raises ------ tanjun.errors.CommandError To end the command's execution with an error response message. tanjun.errors.HaltExecution To indicate that the client should stop searching for commands to execute with the current context. """ @abc.abstractmethod async def close(self, *, unbind: bool = False) -> None: """Close the component. Other Parameters ---------------- unbind : bool Whether to unbind from the client after this is closed. Defaults to `False`. Raises ------ RuntimeError If the component isn't running. """ @abc.abstractmethod async def open(self) -> None: """Start the component. Raises ------ RuntimeError If the component is already open. If the component isn't bound to a client. """ class ClientCallbackNames(str, enum.Enum): """Enum of the standard client callback names. These should be dispatched by all `Client` implementations. """ CLOSED = "closed" """Called when the client has finished closing. No positional arguments are provided for this event. """ CLOSING = "closing" """Called when the client is initially instructed to close. No positional arguments are provided for this event. """ COMPONENT_ADDED = "component_added" """Called when a component is added to an active client. .. warning:: This event isn't dispatched for components which were registered while the client is inactive. The first positional argument is the `tanjun.abc.Component` being added. """ COMPONENT_REMOVED = "component_removed" """Called when a component is added to an active client. .. warning:: This event isn't dispatched for components which were removed while the client is inactive. The first positional argument is the `tanjun.abc.Component` being removed. """ MESSAGE_COMMAND_NOT_FOUND = "message_command_not_found" """Called when a message command is not found. `tanjun.abc.MessageContext` is provided as the first positional argument. """ SLASH_COMMAND_NOT_FOUND = "slash_command_not_found" """Called when a slash command is not found. `tanjun.abc.MessageContext` is provided as the first positional argument. """ STARTED = "started" """Called when the client has finished starting. No positional arguments are provided for this event. """ STARTING = "starting" """Called when the client is initially instructed to start. No positional arguments are provided for this event. """ class Client(abc.ABC): """Abstract interface of a Tanjun client. This should manage both message and slash command execution based on the provided hikari clients. """ __slots__ = () @property @abc.abstractmethod def cache(self) -> typing.Optional[hikari.api.Cache]: """Hikari cache instance this command client was initialised with.""" @property @abc.abstractmethod def components(self) -> collections.Collection[Component]: """Collection of the components this command client is using.""" @property @abc.abstractmethod def defaults_to_ephemeral(self) -> bool: """Whether slash contexts spawned by this client should default to ephemeral responses. This effects calls to `SlashContext.create_followup`, `SlashContext.create_initial_response`, `SlashContext.defer` and `SlashContext.respond` unless the `flags` field is provided for the methods which support it. Notes ----- * This may be overridden by `BaseSlashCommand.defaults_to_ephemeral` and `Component.defaults_to_ephemeral`. * This defaults to `False`. * This only effects slash command execution. """ @property @abc.abstractmethod def events(self) -> typing.Optional[hikari.api.EventManager]: """Object of the event manager this client was initialised with. This is used for executing message commands if set. """ @property @abc.abstractmethod def is_alive(self) -> bool: """Whether this client is alive.""" @property # TODO: switch over to a mapping of event to collection cause convenience @abc.abstractmethod def listeners(self) -> collections.Mapping[type[hikari.Event], collections.Collection[ListenerCallbackSig]]: """Mapping of event types to the listeners registered in this client.""" @property @abc.abstractmethod def loop(self) -> typing.Optional[asyncio.AbstractEventLoop]: """The loop this client is bound to if it's alive.""" @property @abc.abstractmethod def metadata(self) -> collections.MutableMapping[typing.Any, typing.Any]: """Mutable mapping of the metadata set for this client. .. note:: Any modifications made to this mutable mapping will be preserved by the client. """ @property @abc.abstractmethod def prefixes(self) -> collections.Collection[str]: """Collection of the prefixes set for this client. These are only use during message command execution to match commands to this command client. """ @property @abc.abstractmethod def rest(self) -> hikari.api.RESTClient: """Object of the Hikari REST client this client was initialised with.""" @property @abc.abstractmethod def server(self) -> typing.Optional[hikari.api.InteractionServer]: """Object of the Hikari interaction server provided for this client. This is used for executing slash commands if set. """ @property @abc.abstractmethod def shards(self) -> typing.Optional[hikari_traits.ShardAware]: """Object of the Hikari shard manager this client was initialised with.""" @property def voice(self) -> typing.Optional[hikari.api.VoiceComponent]: """Object of the Hikari voice component this client was initialised with.""" @abc.abstractmethod async def clear_application_commands( self, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, ) -> None: """Clear the commands declared either globally or for a specific guild. .. note:: The endpoint this uses has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally). Other Parameters ---------------- application : typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialApplication]] The application to clear commands for. If left as `None` then this will be inferred from the authorization being used by `Client.rest`. guild : hikari.UndefinedOr[hikari.snowflakes.SnowflakeishOr[hikari.PartialGuild]] Object or ID of the guild to clear commands for. If left as `None` global commands will be cleared. """ @abc.abstractmethod async def declare_global_commands( self, command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, force: bool = False, ) -> collections.Sequence[hikari.Command]: """Set the global application commands for a bot based on the loaded components. .. warning:: This will overwrite any previously set application commands and only targets commands marked as global. Notes ----- * The endpoint this uses has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally). * Setting a specific `guild` can be useful for testing/debug purposes as slash commands may take up to an hour to propagate globally but will immediately propagate when set on a specific guild. Other Parameters ---------------- command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] If provided, a mapping of top level command names to IDs of the existing commands to update. application : typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialApplication]] Object or ID of the application to set the global commands for. If left as `None` then this will be inferred from the authorization being used by `Client.rest`. guild : hikari.UndefinedOr[hikari.snowflakes.SnowflakeishOr[hikari.PartialGuild]] Object or ID of the guild to set the global commands to. If left as `None` global commands will be set. force : bool Force this to declare the commands regardless of whether or not they match the current state of the declared commands. Defaults to `False`. This default behaviour helps avoid issues with the 2 request per minute (per-guild or globally) ratelimit and the other limit of only 200 application command creates per day (per guild or globally). Returns ------- collections.abc.Sequence[hikari..Command] API representations of the set commands. """ @abc.abstractmethod async def declare_application_command( self, command: BaseSlashCommand, /, command_id: typing.Optional[hikari.Snowflakeish] = None, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, ) -> hikari.Command: """Declare a single slash command for a bot. .. warning:: Providing `command_id` when updating a command helps avoid any permissions set for the command being lose (e.g. when changing the command's name). Parameters ---------- command : BaseSlashCommand The command to register. Other Parameters ---------------- application : typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialApplication]] The application to register the command with. If left as `None` then this will be inferred from the authorization being used by `Client.rest`. command_id : typing.Optional[hikari.snowflakes.Snowflakeish] ID of the command to update. guild : typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialGuild]] Object or ID of the guild to register the command with. If left as `None` then the command will be registered globally. Returns ------- hikari.Command API representation of the command that was registered. """ @abc.abstractmethod async def declare_application_commands( self, commands: collections.Iterable[BaseSlashCommand], /, command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, force: bool = False, ) -> collections.Sequence[hikari.Command]: """Declare a collection of slash commands for a bot. .. note:: The endpoint this uses has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally). Parameters ---------- commands : collections.abc.Iterable[BaseSlashCommand] Iterable of the commands to register. Other Parameters ---------------- command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] If provided, a mapping of top level command names to IDs of the existing commands to update. While optional, this can be helpful when updating commands as providing the current IDs will prevent changes such as renames from leading to other state set for commands (e.g. permissions) from being lost. application : typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialApplication]] The application to register the commands with. If left as `None` then this will be inferred from the authorization being used by `Client.rest`. guild : typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialGuild]] Object or ID of the guild to register the commands with. If left as `None` then the commands will be registered globally. force : bool Force this to declare the commands regardless of whether or not they match the current state of the declared commands. Defaults to `False`. This default behaviour helps avoid issues with the 2 request per minute (per-guild or globally) ratelimit and the other limit of only 200 application command creates per day (per guild or globally). Returns ------- collections.abc.Sequence[hikari.Command] API representations of the commands which were registered. Raises ------ ValueError Raises a value error for any of the following reasons: * If conflicting command names are found (multiple commanbds have the same top-level name). * If more than 100 top-level commands are passed. """ @abc.abstractmethod def set_metadata(self: _T, key: typing.Any, value: typing.Any, /) -> _T: """Set a field in the client's metadata. Parameters ---------- key : typing.Any Metadata key to set. value : typing.Any Metadata value to set. Returns ------- Self The client instance to enable chained calls. """ @abc.abstractmethod def add_component(self: _T, component: Component, /) -> _T: """Add a component to this client. Parameters ---------- component: Component The component to move to this client. Returns ------- Self The client instance to allow chained calls. """ @abc.abstractmethod def get_component_by_name(self, name: str, /) -> typing.Optional[Component]: """Get a component from this client by name. Parameters ---------- name : str Name to get a component by. Returns ------- typing.Optional[Component] The component instance if found, else `None`. """ @abc.abstractmethod def remove_component(self: _T, component: Component, /) -> _T: """Remove a component from this client. This will unsubscribe any client callbacks, commands and listeners registered in the provided component. Parameters ---------- component: Component The component to remove from this client. Raises ------ ValueError If the provided component isn't found. Returns ------- Self The client instance to allow chained calls. """ @abc.abstractmethod def remove_component_by_name(self: _T, name: str, /) -> _T: """Remove a component from this client by name. This will unsubscribe any client callbacks, commands and listeners registered in the provided component. Parameters ---------- name: str Name of the component to remove from this client. Raises ------ KeyError If the provided component name isn't found. """ @abc.abstractmethod def add_client_callback(self: _T, name: typing.Union[str, ClientCallbackNames], callback: MetaEventSig, /) -> _T: """Add a client callback. Parameters ---------- name : typing.Union[str, ClientCallbackNames] The name this callback is being registered to. This is case-insensitive. callback : MetaEventSigT The callback to register. This may be sync or async and must return None. The positional and keyword arguments a callback should expect depend on implementation detail around the `name` being subscribed to. Returns ------- Self The client instance to enable chained calls. """ @abc.abstractmethod async def dispatch_client_callback( self, name: typing.Union[str, ClientCallbackNames], /, *args: typing.Any ) -> None: """Dispatch a client callback. Parameters ---------- name : typing.Union[str, ClientCallbackNames] The name of the callback to dispatch. Other Parameters ---------------- *args : typing.Any Positional arguments to pass to the callback(s). Raises ------ KeyError If no callbacks are registered for the given name. """ @abc.abstractmethod def get_client_callbacks( self, name: typing.Union[str, ClientCallbackNames], / ) -> collections.Collection[MetaEventSig]: """Get a collection of the callbacks registered for a specific name. Parameters ---------- name : typing.Union[str, ClientCallbackNames] The name to get the callbacks registered for. This is case-insensitive. Returns ------- collections.abc.Collection[MetaEventSig] Collection of the callbacks for the provided name. """ @abc.abstractmethod def remove_client_callback(self: _T, name: typing.Union[str, ClientCallbackNames], callback: MetaEventSig, /) -> _T: """Remove a client callback. Parameters ---------- name : typing.Union[str, ClientCallbackNames] The name this callback is being registered to. This is case-insensitive. callback : MetaEventSigT The callback to remove from the client's callbacks. Raises ------ KeyError If the provided name isn't found. ValueError If the provided callback isn't found. Returns ------- Self The client instance to enable chained calls. """ @abc.abstractmethod def with_client_callback( self, name: typing.Union[str, ClientCallbackNames], / ) -> collections.Callable[[MetaEventSigT], MetaEventSigT]: """Add a client callback through a decorator call. Examples -------- ```py client = tanjun.Client.from_rest_bot(bot) @client.with_client_callback("closed") async def on_close() -> None: raise NotImplementedError ``` Parameters ---------- name : typing.Union[str, ClientCallbackNames] The name this callback is being registered to. This is case-insensitive. Returns ------- collections.abc.Callable[[MetaEventSigT], MetaEventSigT] Decorator callback used to register the client callback. This may be sync or async and must return None. The positional and keyword arguments a callback should expect depend on implementation detail around the `name` being subscribed to. """ @abc.abstractmethod def add_listener(self: _T, event_type: type[hikari.Event], callback: ListenerCallbackSig, /) -> _T: """Add a listener to the client. Parameters ---------- event_type : type[hikari.Event] The event type to add a listener for. callback: ListenerCallbackSig The callback to register as a listener. This callback must be a coroutine function which returns `None` and always takes at least one positional arg of type `hikari.Event` regardless of client implementation detail. Returns ------- Self The client instance to enable chained calls. """ @abc.abstractmethod def remove_listener(self: _T, event_type: type[hikari.Event], callback: ListenerCallbackSig, /) -> _T: """Remove a listener from the client. Parameters ---------- event_type : type[hikari.Event] The event type to remove a listener for. callback: ListenerCallbackSig The callback to remove. Raises ------ KeyError If the provided event type isn't found. ValueError If the provided callback isn't found. Returns ------- Self The client instance to enable chained calls. """ @abc.abstractmethod def with_listener( self, event_type: type[hikari.Event], / ) -> collections.Callable[[ListenerCallbackSigT], ListenerCallbackSigT]: """Add an event listener to this client through a decorator call. Examples -------- ```py client = tanjun.Client.from_gateway_bot(bot) @client.with_listener(hikari.MessageCreateEvent) async def on_message_create(event: hikari.MessageCreateEvent) -> None: raise NotImplementedError ``` Parameters ---------- event_type : type[hikari.Event] The event type to listener for. Returns ------- collections.abc.Callable[[ListenerCallbackSigT], ListenerCallbackSigT] Decorator callback used to register the event callback. The callback must be a coroutine function which returns `None` and always takes at least one positional arg of type `hikari.Event` regardless of client implementation detail. """ @abc.abstractmethod def iter_commands(self) -> collections.Iterator[ExecutableCommand[Context]]: """Iterate over all the commands (both message and slash) registered to this client. Returns ------- collections.abc.Iterator[ExecutableCommand[Context]] Iterator of all the commands registered to this client. """ @abc.abstractmethod def iter_message_commands(self) -> collections.Iterator[MessageCommand[typing.Any]]: """Iterate over all the message commands registered to this client. Returns ------- collections.abc.Iterator[MessageCommand] Iterator of all the message commands registered to this client. """ @abc.abstractmethod def iter_slash_commands(self, *, global_only: bool = False) -> collections.Iterator[BaseSlashCommand]: """Iterate over all the slash commands registered to this client. Parameters ---------- global_only : bool Whether to only iterate over global slash commands. Returns ------- collections.abc.Iterator[BaseSlashCommand] Iterator of all the slash commands registered to this client. """ @abc.abstractmethod def check_message_name(self, name: str, /) -> collections.Iterator[tuple[str, MessageCommand[typing.Any]]]: """Check whether a message command name is present in the current client. .. note:: Dependent on implementation this may partial check name against the message command's name based on command_name.startswith(name). Parameters ---------- name : str The name to match commands against. Returns ------- collections.abc.Iterator[tuple[str, MessageCommand]] Iterator of the matched command names to the matched message command objects. """ @abc.abstractmethod def check_slash_name(self, name: str, /) -> collections.Iterator[BaseSlashCommand]: """Check whether a slash command name is present in the current client. .. note:: This won't check the commands within command groups. Parameters ---------- name : str Name to check against. Returns ------- collections.abc.Iterator[BaseSlashCommand] Iterator of the matched slash command objects. """ @abc.abstractmethod def load_modules(self: _T, *modules: typing.Union[str, pathlib.Path]) -> _T: """Load entities into this client from modules based on present loaders. .. note:: If an `__all__` is present in the target module then it will be used to find loaders. Examples -------- For this to work the target module has to have at least one loader present. ```py @tanjun.as_loader def load_module(client: tanjun.Client) -> None: client.add_component(component.copy()) ``` or ```py loader = tanjun.Component("trans component").load_from_scope().make_loader() ``` Parameters ---------- *modules : typing.Union[str, pathlib.Path] Path(s) of the modules to load from. When `str` this will be treated as a normal import path which is absolute (`"foo.bar.baz"`). It's worth noting that absolute module paths may be imported from the current location if the top level module is a valid module file or module directory in the current working directory. When `pathlib.Path` the module will be imported directly from the given path. In this mode any relative imports in the target module will fail to resolve. Returns ------- Self This client instance to enable chained calls. Raises ------ tanjun.errors.FailedModuleLoad If the new version of a module failed to load. This includes if it failed to import or if one of its loaders raised. The source error can be found at `tanjun.errors.FailedModuleLoad.__source__`. tanjun.errors.ModuleStateConflict If the module is already loaded. tanjun.errors.ModuleMissingLoaders If no loaders are found in the module. ModuleNotFoundError If the module is not found. """ @abc.abstractmethod async def load_modules_async(self, *modules: typing.Union[str, pathlib.Path]) -> None: """Asynchronous variant of `Client.load_modules`. Unlike `Client.load_modules`, this method will run blocking code in a background thread. For more information on the behaviour of this method see the documentation for `Client.load_modules`. """ @abc.abstractmethod def unload_modules(self: _T, *modules: typing.Union[str, pathlib.Path]) -> _T: """Unload entities from this client based on unloaders in one or more modules. .. note:: If an `__all__` is present in the target module then it will be used to find unloaders. Examples -------- For this to work the module has to have at least one unloading enabled `tanjun.abc.ClientLoader` present. ```py @tanjun.as_unloader def unload_component(client: tanjun.Client) -> None: client.remove_component_by_name(component.name) ``` or ```py # as_loader's returned ClientLoader handles both loading and unloading. loader = tanjun.Component("trans component").load_from_scope().as_loader(unload_component) ``` Parameters ---------- *modules: typing.Union[str, pathlib.Path] Path of one or more modules to unload. These should be the same path(s) which were passed to `load_module`. Returns ------- Self This client instance to enable chained calls. Raises ------ tanjun.errors.ModuleStateConflict If the module hasn't been loaded. tanjun.errors.ModuleMissingLoaders If no unloaders are found in the module. tanjun.errors.FailedModuleUnload If the old version of a module failed to unload. This indicates that one of its unloaders raised. The source error can be found at `tanjun.errors.FailedModuleUnload.__source__`. """ @abc.abstractmethod def reload_modules(self: _T, *modules: typing.Union[str, pathlib.Path]) -> _T: """Reload entities in this client based on the loaders in loaded module(s). .. note:: If an `__all__` is present in the target module then it will be used to find loaders and unloaders. Examples -------- For this to work the module has to have at least one ClientLoader which handles loading and one which handles unloading present. Parameters ---------- *modules: typing.Union[str, pathlib.Path] Paths of one or more module to unload. These should be the same paths which were passed to `load_module`. Returns ------- Self This client instance to enable chained calls. Raises ------ tanjun.errors.FailedModuleLoad If the new version of a module failed to load. This includes if it failed to import or if one of its loaders raised. The source error can be found at `tanjun.errors.FailedModuleLoad.__source__`. tanjun.errors.FailedModuleUnload If the old version of a module failed to unload. This indicates that one of its unloaders raised. The source error can be found at `tanjun.errors.FailedModuleUnload.__source__`. tanjun.errors.ModuleStateConflict If the module hasn't been loaded. tanjun.errors.ModuleMissingLoaders If no unloaders are found in the current state of the module. If no loaders are found in the new state of the module. ModuleNotFoundError If the module can no-longer be found at the provided path. """ @abc.abstractmethod async def reload_modules_async(self, *modules: typing.Union[str, pathlib.Path]) -> None: """Asynchronous variant of `Client.reload_modules`. Unlike `Client.reload_modules`, this method will run blocking code in a background thread. For more information on the behaviour of this method see the documentation for `Client.reload_modules`. """ class ClientLoader(abc.ABC): """Interface of logic used to load and unload components into a generic client.""" __slots__ = () @property @abc.abstractmethod def has_load(self) -> bool: """Whether this loader will load anything.""" @property @abc.abstractmethod def has_unload(self) -> bool: """Whether this loader will unload anything.""" @abc.abstractmethod def load(self, client: Client, /) -> bool: """Load logic into a client instance. Parameters ---------- client : Client The client to load commands and listeners for. Returns ------- bool Whether anything was loaded. """ @abc.abstractmethod def unload(self, client: Client, /) -> bool: """Unload logic from a client instance. Parameters ---------- client : Client The client to unload commands and listeners from. Returns ------- bool Whether anything was unloaded. """
Interfaces of the objects and clients used within Tanjun.
View Source
class ClientCallbackNames(str, enum.Enum): """Enum of the standard client callback names. These should be dispatched by all `Client` implementations. """ CLOSED = "closed" """Called when the client has finished closing. No positional arguments are provided for this event. """ CLOSING = "closing" """Called when the client is initially instructed to close. No positional arguments are provided for this event. """ COMPONENT_ADDED = "component_added" """Called when a component is added to an active client. .. warning:: This event isn't dispatched for components which were registered while the client is inactive. The first positional argument is the `tanjun.abc.Component` being added. """ COMPONENT_REMOVED = "component_removed" """Called when a component is added to an active client. .. warning:: This event isn't dispatched for components which were removed while the client is inactive. The first positional argument is the `tanjun.abc.Component` being removed. """ MESSAGE_COMMAND_NOT_FOUND = "message_command_not_found" """Called when a message command is not found. `tanjun.abc.MessageContext` is provided as the first positional argument. """ SLASH_COMMAND_NOT_FOUND = "slash_command_not_found" """Called when a slash command is not found. `tanjun.abc.MessageContext` is provided as the first positional argument. """ STARTED = "started" """Called when the client has finished starting. No positional arguments are provided for this event. """ STARTING = "starting" """Called when the client is initially instructed to start. No positional arguments are provided for this event. """
Enum of the standard client callback names.
These should be dispatched by all Client implementations.
Called when the client has finished closing.
No positional arguments are provided for this event.
Called when the client is initially instructed to close.
No positional arguments are provided for this event.
Called when a component is added to an active client.
Warning: This event isn't dispatched for components which were registered while the client is inactive.
The first positional argument is the tanjun.abc.Component being added.
Called when a component is added to an active client.
Warning: This event isn't dispatched for components which were removed while the client is inactive.
The first positional argument is the tanjun.abc.Component being removed.
Called when a message command is not found.
tanjun.abc.MessageContext is provided as the first positional argument.
Called when a slash command is not found.
tanjun.abc.MessageContext is provided as the first positional argument.
Called when the client has finished starting.
No positional arguments are provided for this event.
Called when the client is initially instructed to start.
No positional arguments are provided for this event.
Inherited Members
- enum.Enum
- name
- value
- builtins.str
- encode
- replace
- split
- rsplit
- join
- capitalize
- casefold
- title
- center
- count
- expandtabs
- find
- partition
- index
- ljust
- lower
- lstrip
- rfind
- rindex
- rjust
- rstrip
- rpartition
- splitlines
- strip
- swapcase
- translate
- upper
- startswith
- endswith
- removeprefix
- removesuffix
- isascii
- islower
- isupper
- istitle
- isspace
- isdecimal
- isdigit
- isnumeric
- isalpha
- isalnum
- isidentifier
- isprintable
- zfill
- format
- format_map
- maketrans
View Source
# -*- coding: utf-8 -*- # cython: language_level=3 # BSD 3-Clause License # # Copyright (c) 2020-2022, Faster Speeding # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """A collection of common standard checks designed for Tanjun commands.""" from __future__ import annotations __all__: list[str] = [ "all_checks", "any_checks", "CallbackReturnT", "CommandT", "with_all_checks", "with_any_checks", "with_check", "with_dm_check", "with_guild_check", "with_nsfw_check", "with_sfw_check", "with_owner_check", "with_author_permission_check", "with_own_permission_check", "DmCheck", "GuildCheck", "NsfwCheck", "SfwCheck", "OwnerCheck", "AuthorPermissionCheck", "OwnPermissionCheck", ] import typing from collections import abc as collections import hikari from . import dependencies from . import errors from . import injecting from . import utilities if typing.TYPE_CHECKING: from . import abc as tanjun_abc CommandT = typing.TypeVar("CommandT", bound="tanjun_abc.ExecutableCommand[typing.Any]") # This errors on earlier 3.9 releases when not quotes cause dumb handling of the [CommandT] list CallbackReturnT = typing.Union[CommandT, "collections.Callable[[CommandT], CommandT]"] """Type hint for the return value of decorators which optionally take keyword arguments. Examples -------- Decorator functions with this as their return type may either be used as a decorator directly without being explicitly called: ```python @with_dm_check @as_command("foo") def foo_command(self, ctx: Context) -> None: raise NotImplemented ``` Or may be called with the listed other parameters as keyword arguments while decorating a function. ```python @with_dm_check(halt_execution=True) @as_command("foo") def foo_command(self, ctx: Context) -> None: raise NotImplemented ``` """ class InjectableCheck(injecting.CallbackDescriptor[bool]): __slots__ = () async def __call__(self, ctx: tanjun_abc.Context, /) -> bool: if result := await self.resolve_with_command_context(ctx, ctx): return result raise errors.FailedCheck def _optional_kwargs( command: typing.Optional[CommandT], check: tanjun_abc.CheckSig, / ) -> typing.Union[CommandT, collections.Callable[[CommandT], CommandT]]: if command: return command.add_check(check) return lambda c: c.add_check(check) class _Check: __slots__ = ("_error_message", "_halt_execution") def __init__( self, error_message: typing.Optional[str], halt_execution: bool, ) -> None: self._error_message = error_message self._halt_execution = halt_execution def _handle_result(self, result: bool) -> bool: if not result: if self._error_message: raise errors.CommandError(self._error_message) from None if self._halt_execution: raise errors.HaltExecution from None return result class OwnerCheck(_Check): """Standard owner check callback registered by `with_owner_check`. This check will only pass if the author of the command is a bot owner. """ __slots__ = () def __init__( self, *, error_message: typing.Optional[str] = "Only bot owners can use this command", halt_execution: bool = False, ) -> None: """Initialise a owner check. .. note:: error_message takes priority over halt_execution. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "Only bot owners can use this command" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. """ super().__init__(error_message, halt_execution) async def __call__( self, ctx: tanjun_abc.Context, dependency: dependencies.AbstractOwners = injecting.inject(type=dependencies.AbstractOwners), ) -> bool: return self._handle_result(await dependency.check_ownership(ctx.client, ctx.author)) _GuildChannelCacheT = typing.Optional[dependencies.SfCache[hikari.GuildChannel]] async def _get_is_nsfw( ctx: tanjun_abc.Context, /, *, dm_default: bool, channel_cache: _GuildChannelCacheT, ) -> bool: if ctx.guild_id is None: return dm_default channel: typing.Optional[hikari.PartialChannel] = None if ctx.cache and (channel := ctx.cache.get_guild_channel(ctx.channel_id)): return channel.is_nsfw or False if channel_cache: try: return (await channel_cache.get(ctx.channel_id)).is_nsfw or False except dependencies.EntryNotFound: raise except dependencies.CacheMissError: pass channel = await ctx.rest.fetch_channel(ctx.channel_id) assert isinstance(channel, hikari.GuildChannel) return channel.is_nsfw or False class NsfwCheck(_Check): """Standard NSFW check callback registered by `with_nsfw_check`. This check will only pass if the current channel is NSFW. """ __slots__ = () def __init__( self, *, error_message: typing.Optional[str] = "Command can only be used in NSFW channels", halt_execution: bool = False, ) -> None: """Initialise a NSFW check. .. note:: error_message takes priority over halt_execution. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "Command can only be used in NSFW channels" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. """ super().__init__(error_message, halt_execution) async def __call__( self, ctx: tanjun_abc.Context, /, channel_cache: _GuildChannelCacheT = injecting.inject(type=_GuildChannelCacheT), ) -> bool: return self._handle_result(await _get_is_nsfw(ctx, dm_default=True, channel_cache=channel_cache)) class SfwCheck(_Check): """Standard SFW check callback registered by `with_sfw_check`. This check will only pass if the current channel is SFW. """ __slots__ = () def __init__( self, *, error_message: typing.Optional[str] = "Command can only be used in SFW channels", halt_execution: bool = False, ) -> None: """Initialise a SFW check. .. note:: error_message takes priority over halt_execution. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "Command can only be used in SFW channels" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. """ super().__init__(error_message, halt_execution) async def __call__( self, ctx: tanjun_abc.Context, /, channel_cache: _GuildChannelCacheT = injecting.inject(type=_GuildChannelCacheT), ) -> bool: return self._handle_result(not await _get_is_nsfw(ctx, dm_default=False, channel_cache=channel_cache)) class DmCheck(_Check): """Standard DM check callback registered by `with_dm_check`. This check will only pass if the current channel is a DM channel. """ __slots__ = () def __init__( self, *, error_message: typing.Optional[str] = "Command can only be used in DMs", halt_execution: bool = False, ) -> None: """Initialise a DM check. .. note:: error_message takes priority over halt_execution. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "Command can only be used in DMs" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. """ super().__init__(error_message, halt_execution) def __call__(self, ctx: tanjun_abc.Context, /) -> bool: return self._handle_result(ctx.guild_id is None) class GuildCheck(_Check): """Standard guild check callback registered by `with_guild_check`. This check will only pass if the current channel is in a guild. """ __slots__ = () def __init__( self, *, error_message: typing.Optional[str] = "Command can only be used in guild channels", halt_execution: bool = False, ) -> None: """Initialise a guild check. .. note:: error_message takes priority over halt_execution. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "Command can only be used in guild channels" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. """ super().__init__(error_message, halt_execution) def __call__(self, ctx: tanjun_abc.Context, /) -> bool: return self._handle_result(ctx.guild_id is not None) class AuthorPermissionCheck(_Check): """Standard author permission check callback registered by `with_author_permission_check`. This check will only pass if the current author has the specified permission. """ __slots__ = ("_permissions",) def __init__( self, permissions: typing.Union[hikari.Permissions, int], /, *, error_message: typing.Optional[str] = "You don't have the permissions required to use this command", halt_execution: bool = False, ) -> None: """Initialise an author permission check. .. note:: error_message takes priority over halt_execution. Parameters ---------- permissions: typing.Union[hikari.permissions.Permissions, int] The permission(s) required for this command to run. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "You don't have the permissions required to use this command" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. """ super().__init__(error_message=error_message, halt_execution=halt_execution) self._permissions = permissions async def __call__(self, ctx: tanjun_abc.Context, /) -> bool: if not ctx.member: # If there's no member when this is within a guild then it's likely # something like a webhook or guild visitor with no real permissions # outside of some basic set of send messages. if ctx.guild_id: permissions = await utilities.fetch_everyone_permissions( ctx.client, ctx.guild_id, channel=ctx.channel_id ) else: permissions = utilities.DM_PERMISSIONS elif isinstance(ctx.member, hikari.InteractionMember): permissions = ctx.member.permissions else: permissions = await utilities.fetch_permissions(ctx.client, ctx.member, channel=ctx.channel_id) return self._handle_result((self._permissions & permissions) == self._permissions) class OwnPermissionCheck(_Check): """Standard own permission check callback registered by `with_own_permission_check`. This check will only pass if the current bot user has the specified permission. """ __slots__ = ("_permissions",) def __init__( self, permissions: typing.Union[hikari.Permissions, int], /, *, error_message: typing.Optional[str] = "Bot doesn't have the permissions required to run this command", halt_execution: bool = False, ) -> None: """Initialise a own permission check. .. note:: error_message takes priority over halt_execution. Parameters ---------- permissions: typing.Union[hikari.permissions.Permissions, int] The permission(s) required for this command to run. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "Bot doesn't have the permissions required to run this command" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. """ super().__init__(error_message=error_message, halt_execution=halt_execution) self._permissions = permissions async def __call__( self, ctx: tanjun_abc.Context, /, my_user: hikari.OwnUser = dependencies.inject_lc(hikari.OwnUser), ) -> bool: if ctx.guild_id is None: permissions = utilities.DM_PERMISSIONS elif ctx.cache and (member := ctx.cache.get_member(ctx.guild_id, my_user)): permissions = await utilities.fetch_permissions(ctx.client, member, channel=ctx.channel_id) else: try: member = await ctx.rest.fetch_member(ctx.guild_id, my_user.id) except hikari.NotFoundError: # If we're not in the Guild then we have to assume the application # is still in there and that we likely won't be able to do anything. # TODO: re-visit this later. return self._handle_result(False) permissions = await utilities.fetch_permissions(ctx.client, member, channel=ctx.channel_id) return self._handle_result((permissions & self._permissions) == self._permissions) @typing.overload def with_dm_check(command: CommandT, /) -> CommandT: ... @typing.overload def with_dm_check( *, error_message: typing.Optional[str] = "Command can only be used in DMs", halt_execution: bool = False ) -> collections.Callable[[CommandT], CommandT]: ... def with_dm_check( command: typing.Optional[CommandT] = None, /, *, error_message: typing.Optional[str] = "Command can only be used in DMs", halt_execution: bool = False, ) -> CallbackReturnT[CommandT]: """Only let a command run in a DM channel. Parameters ---------- command : typing.Optional[CommandT] The command to add this check to. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "Command can only be used in DMs" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. Notes ----- * error_message takes priority over halt_execution. * For more information on how this is used with other parameters see `CallbackReturnT`. Returns ------- CallbackReturnT[CommandT] The command this check was added to. """ return _optional_kwargs(command, DmCheck(halt_execution=halt_execution, error_message=error_message)) @typing.overload def with_guild_check(command: CommandT, /) -> CommandT: ... @typing.overload def with_guild_check( *, error_message: typing.Optional[str] = "Command can only be used in guild channels", halt_execution: bool = False ) -> collections.Callable[[CommandT], CommandT]: ... def with_guild_check( command: typing.Optional[CommandT] = None, /, *, error_message: typing.Optional[str] = "Command can only be used in guild channels", halt_execution: bool = False, ) -> CallbackReturnT[CommandT]: """Only let a command run in a guild channel. Parameters ---------- command : typing.Optional[CommandT] The command to add this check to. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "Command can only be used in guild channels" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. Notes ----- * error_message takes priority over halt_execution. * For more information on how this is used with other parameters see `CallbackReturnT`. Returns ------- CallbackReturnT[CommandT] The command this check was added to. """ return _optional_kwargs(command, GuildCheck(halt_execution=halt_execution, error_message=error_message)) @typing.overload def with_nsfw_check(command: CommandT, /) -> CommandT: ... @typing.overload def with_nsfw_check( *, error_message: typing.Optional[str] = "Command can only be used in NSFW channels", halt_execution: bool = False ) -> collections.Callable[[CommandT], CommandT]: ... def with_nsfw_check( command: typing.Optional[CommandT] = None, /, *, error_message: typing.Optional[str] = "Command can only be used in NSFW channels", halt_execution: bool = False, ) -> CallbackReturnT[CommandT]: """Only let a command run in a channel that's marked as nsfw. Parameters ---------- command : typing.Optional[CommandT] The command to add this check to. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "Command can only be used in NSFW channels" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. Notes ----- * error_message takes priority over halt_execution. * For more information on how this is used with other parameters see `CallbackReturnT`. Returns ------- CallbackReturnT[CommandT] The command this check was added to. """ return _optional_kwargs(command, NsfwCheck(halt_execution=halt_execution, error_message=error_message)) @typing.overload def with_sfw_check(command: CommandT, /) -> CommandT: ... @typing.overload def with_sfw_check( *, error_message: typing.Optional[str] = "Command can only be used in SFW channels", halt_execution: bool = False, ) -> collections.Callable[[CommandT], CommandT]: ... def with_sfw_check( command: typing.Optional[CommandT] = None, /, *, error_message: typing.Optional[str] = "Command can only be used in SFW channels", halt_execution: bool = False, ) -> CallbackReturnT[CommandT]: """Only let a command run in a channel that's marked as sfw. Parameters ---------- command : typing.Optional[CommandT] The command to add this check to. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "Command can only be used in SFW channels" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. Notes ----- * error_message takes priority over halt_execution. * For more information on how this is used with other parameters see `CallbackReturnT`. Returns ------- CallbackReturnT[CommandT] The command this check was added to. """ return _optional_kwargs(command, SfwCheck(halt_execution=halt_execution, error_message=error_message)) @typing.overload def with_owner_check(command: CommandT, /) -> CommandT: ... @typing.overload def with_owner_check( *, error_message: typing.Optional[str] = "Only bot owners can use this command", halt_execution: bool = False, ) -> collections.Callable[[CommandT], CommandT]: ... def with_owner_check( command: typing.Optional[CommandT] = None, /, *, error_message: typing.Optional[str] = "Only bot owners can use this command", halt_execution: bool = False, ) -> CallbackReturnT[CommandT]: """Only let a command run if it's being triggered by one of the bot's owners. Parameters ---------- command : typing.Optional[CommandT] The command to add this check to. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "Only bot owners can use this command" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. Notes ----- * error_message takes priority over halt_execution. * For more information on how this is used with other parameters see `CallbackReturnT`. Returns ------- CallbackReturnT[CommandT] The command this check was added to. """ return _optional_kwargs(command, OwnerCheck(halt_execution=halt_execution, error_message=error_message)) def with_author_permission_check( permissions: typing.Union[hikari.Permissions, int], *, error_message: typing.Optional[str] = "You don't have the permissions required to use this command", halt_execution: bool = False, ) -> collections.Callable[[CommandT], CommandT]: """Only let a command run if the author has certain permissions in the current channel. Parameters ---------- permissions: typing.Union[hikari.permissions.Permissions, int] The permission(s) required for this command to run. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "You don't have the permissions required to use this command" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. Notes ----- * error_message takes priority over halt_execution. * This will only pass for commands in DMs if `permissions` is valid for a DM context (e.g. can't have any moderation permissions) Returns ------- collections.abc.Callable[[CommandT], CommandT] A command decorator callback which adds the check. """ return lambda command: command.add_check( AuthorPermissionCheck(permissions, halt_execution=halt_execution, error_message=error_message) ) def with_own_permission_check( permissions: typing.Union[hikari.Permissions, int], *, error_message: typing.Optional[str] = "Bot doesn't have the permissions required to run this command", halt_execution: bool = False, ) -> collections.Callable[[CommandT], CommandT]: """Only let a command run if we have certain permissions in the current channel. Parameters ---------- permissions: typing.Union[hikari.permissions.Permissions, int] The permission(s) required for this command to run. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "Bot doesn't have the permissions required to run this command" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. Notes ----- * error_message takes priority over halt_execution. * This will only pass for commands in DMs if `permissions` is valid for a DM context (e.g. can't have any moderation permissions) Returns ------- collections.abc.Callable[[CommandT], CommandT] A command decorator callback which adds the check. """ return lambda command: command.add_check( OwnPermissionCheck(permissions, halt_execution=halt_execution, error_message=error_message) ) def with_check(check: tanjun_abc.CheckSig, /) -> collections.Callable[[CommandT], CommandT]: """Add a generic check to a command. Parameters ---------- check : tanjun.abc.CheckSig The check to add to this command. Returns ------- collections.abc.Callable[[CommandT], CommandT] A command decorator callback which adds the check. """ return lambda command: command.add_check(check) class _AllChecks(_Check): __slots__ = ("_checks",) def __init__(self, checks: list[injecting.CallbackDescriptor[bool]]) -> None: self._checks = checks async def __call__(self, ctx: tanjun_abc.Context, /) -> bool: for check in self._checks: if not await check.resolve_with_command_context(ctx, ctx): return False return True def all_checks( check: tanjun_abc.CheckSig, /, *checks: tanjun_abc.CheckSig, ) -> collections.Callable[[tanjun_abc.Context], collections.Coroutine[typing.Any, typing.Any, bool]]: """Combine multiple check callbacks into a check which will only pass if all the callbacks pass. This ensures that the callbacks are run in the order they were supplied in rather than concurrently. Parameters ---------- check : typing_abc.CheckSig The first check callback to combine. *checks : typing_abc.CheckSig Additional check callbacks to combine. Returns ------- collections.abc.Callable[[tanjun_abc.Context], collections.abc.Coroutine[typing.Any, typing.Any, bool]] A check which will pass if all of the provided check callbacks pass. """ checks_ = [injecting.CallbackDescriptor(check)] checks_.extend(map(injecting.CallbackDescriptor[bool], checks)) return _AllChecks(checks_) def with_all_checks( check: tanjun_abc.CheckSig, /, *checks: tanjun_abc.CheckSig, ) -> collections.Callable[[CommandT], CommandT]: """Add a check which will pass if all the provided checks pass through a decorator call. This ensures that the callbacks are run in the order they were supplied in rather than concurrently. Parameters ---------- check : typing_abc.CheckSig The first check callback to combine. *checks : typing_abc.CheckSig Additional check callbacks to combine. Returns ------- collections.abc.Callable[[tanjun_abc.Context], collections.abc.Coroutine[typing.Any, typing.Any, bool]] A check which will pass if all of the provided check callbacks pass. """ return lambda c: c.add_check(all_checks(check, *checks)) class _AnyChecks(_Check): __slots__ = ("_checks", "_suppress", "_error_message", "_halt_execution") def __init__( self, checks: list[injecting.CallbackDescriptor[bool]], suppress: tuple[type[Exception], ...], error_message: typing.Optional[str], halt_execution: bool, ) -> None: self._checks = checks self._suppress = suppress self._error_message = error_message self._halt_execution = halt_execution async def __call__(self, ctx: tanjun_abc.Context, /) -> bool: for check in self._checks: try: if await check.resolve_with_command_context(ctx, ctx): return True except errors.FailedCheck: pass except self._suppress: pass if self._error_message is not None: raise errors.CommandError(self._error_message) if self._halt_execution: raise errors.HaltExecution return False def any_checks( check: tanjun_abc.CheckSig, /, *checks: tanjun_abc.CheckSig, suppress: tuple[type[Exception], ...] = (errors.CommandError, errors.HaltExecution), error_message: typing.Optional[str], halt_execution: bool = False, ) -> collections.Callable[[tanjun_abc.Context], collections.Coroutine[typing.Any, typing.Any, bool]]: """Combine multiple checks into a check which'll pass if any of the callbacks pass. This ensures that the callbacks are run in the order they were supplied in rather than concurrently. Parameters ---------- check : typing_abc.CheckSig The first check callback to combine. *checks : typing_abc.CheckSig Additional check callbacks to combine. error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. This takes priority over `halt_execution`. Other Parameters ---------------- suppress : tuple[type[Exception], ...] Tuple of the exceptions to suppress when a check fails. Defaults to (`tanjun.errors.CommandError`, `tanjun.errors.HaltExecution`). halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. Returns ------- collections.Callable[[CommandT], CommandT] A decorator which adds the generated check to a command. """ checks_ = [injecting.CallbackDescriptor(check)] checks_.extend(map(injecting.CallbackDescriptor[bool], checks)) return _AnyChecks(checks_, suppress, error_message, halt_execution) def with_any_checks( check: tanjun_abc.CheckSig, /, *checks: tanjun_abc.CheckSig, suppress: tuple[type[Exception], ...] = (errors.CommandError, errors.HaltExecution), error_message: typing.Optional[str], halt_execution: bool = False, ) -> collections.Callable[[CommandT], CommandT]: """Add a check which'll pass if any of the provided checks pass through a decorator call. This ensures that the callbacks are run in the order they were supplied in rather than concurrently. Parameters ---------- check : typing_abc.CheckSig The first check callback to combine. *checks : typing_abc.CheckSig Additional check callbacks to combine. error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. This takes priority over `halt_execution`. Other Parameters ---------------- suppress : tuple[type[Exception], ...] Tuple of the exceptions to suppress when a check fails. Defaults to (`tanjun.errors.CommandError`, `tanjun.errors.HaltExecution`). halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. Returns ------- collections.Callable[[CommandT], CommandT] A decorator which adds the generated check to a command. """ return lambda c: c.add_check( any_checks(check, *checks, suppress=suppress, error_message=error_message, halt_execution=halt_execution) )
A collection of common standard checks designed for Tanjun commands.
View Source
def with_all_checks( check: tanjun_abc.CheckSig, /, *checks: tanjun_abc.CheckSig, ) -> collections.Callable[[CommandT], CommandT]: """Add a check which will pass if all the provided checks pass through a decorator call. This ensures that the callbacks are run in the order they were supplied in rather than concurrently. Parameters ---------- check : typing_abc.CheckSig The first check callback to combine. *checks : typing_abc.CheckSig Additional check callbacks to combine. Returns ------- collections.abc.Callable[[tanjun_abc.Context], collections.abc.Coroutine[typing.Any, typing.Any, bool]] A check which will pass if all of the provided check callbacks pass. """ return lambda c: c.add_check(all_checks(check, *checks))
Add a check which will pass if all the provided checks pass through a decorator call.
This ensures that the callbacks are run in the order they were supplied in rather than concurrently.
Parameters
- check (typing_abc.CheckSig): The first check callback to combine.
- *checks (typing_abc.CheckSig): Additional check callbacks to combine.
Returns
- collections.abc.Callable[[tanjun_abc.Context], collections.abc.Coroutine[typing.Any, typing.Any, bool]]: A check which will pass if all of the provided check callbacks pass.
View Source
def with_any_checks( check: tanjun_abc.CheckSig, /, *checks: tanjun_abc.CheckSig, suppress: tuple[type[Exception], ...] = (errors.CommandError, errors.HaltExecution), error_message: typing.Optional[str], halt_execution: bool = False, ) -> collections.Callable[[CommandT], CommandT]: """Add a check which'll pass if any of the provided checks pass through a decorator call. This ensures that the callbacks are run in the order they were supplied in rather than concurrently. Parameters ---------- check : typing_abc.CheckSig The first check callback to combine. *checks : typing_abc.CheckSig Additional check callbacks to combine. error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. This takes priority over `halt_execution`. Other Parameters ---------------- suppress : tuple[type[Exception], ...] Tuple of the exceptions to suppress when a check fails. Defaults to (`tanjun.errors.CommandError`, `tanjun.errors.HaltExecution`). halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. Returns ------- collections.Callable[[CommandT], CommandT] A decorator which adds the generated check to a command. """ return lambda c: c.add_check( any_checks(check, *checks, suppress=suppress, error_message=error_message, halt_execution=halt_execution) )
Add a check which'll pass if any of the provided checks pass through a decorator call.
This ensures that the callbacks are run in the order they were supplied in rather than concurrently.
Parameters
- check (typing_abc.CheckSig): The first check callback to combine.
- *checks (typing_abc.CheckSig): Additional check callbacks to combine.
error_message (typing.Optional[str]): The error message to send in response as a command error if the check fails.
This takes priority over
halt_execution.
Other Parameters
suppress (tuple[type[Exception], ...]): Tuple of the exceptions to suppress when a check fails.
Defaults to (
tanjun.errors.CommandError,tanjun.errors.HaltExecution).halt_execution (bool): Whether this check should raise
tanjun.errors.HaltExecutionto end the execution search when it fails instead of returningFalse.Defaults to
False.
Returns
- collections.Callable[[CommandT], CommandT]: A decorator which adds the generated check to a command.
View Source
def with_check(check: tanjun_abc.CheckSig, /) -> collections.Callable[[CommandT], CommandT]: """Add a generic check to a command. Parameters ---------- check : tanjun.abc.CheckSig The check to add to this command. Returns ------- collections.abc.Callable[[CommandT], CommandT] A command decorator callback which adds the check. """ return lambda command: command.add_check(check)
Add a generic check to a command.
Parameters
- check (tanjun.abc.CheckSig): The check to add to this command.
Returns
- collections.abc.Callable[[CommandT], CommandT]: A command decorator callback which adds the check.
View Source
def with_dm_check( command: typing.Optional[CommandT] = None, /, *, error_message: typing.Optional[str] = "Command can only be used in DMs", halt_execution: bool = False, ) -> CallbackReturnT[CommandT]: """Only let a command run in a DM channel. Parameters ---------- command : typing.Optional[CommandT] The command to add this check to. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "Command can only be used in DMs" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. Notes ----- * error_message takes priority over halt_execution. * For more information on how this is used with other parameters see `CallbackReturnT`. Returns ------- CallbackReturnT[CommandT] The command this check was added to. """ return _optional_kwargs(command, DmCheck(halt_execution=halt_execution, error_message=error_message))
Only let a command run in a DM channel.
Parameters
- command (typing.Optional[CommandT]): The command to add this check to.
Other Parameters
error_message (typing.Optional[str]): The error message to send in response as a command error if the check fails.
Defaults to "Command can only be used in DMs" and setting this to
Nonewill disable the error message allowing the command search to continue.halt_execution (bool): Whether this check should raise
tanjun.errors.HaltExecutionto end the execution search when it fails instead of returningFalse.Defaults to
False.
Notes
- error_message takes priority over halt_execution.
- For more information on how this is used with other parameters see
CallbackReturnT.
Returns
- CallbackReturnT[CommandT]: The command this check was added to.
View Source
def with_guild_check( command: typing.Optional[CommandT] = None, /, *, error_message: typing.Optional[str] = "Command can only be used in guild channels", halt_execution: bool = False, ) -> CallbackReturnT[CommandT]: """Only let a command run in a guild channel. Parameters ---------- command : typing.Optional[CommandT] The command to add this check to. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "Command can only be used in guild channels" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. Notes ----- * error_message takes priority over halt_execution. * For more information on how this is used with other parameters see `CallbackReturnT`. Returns ------- CallbackReturnT[CommandT] The command this check was added to. """ return _optional_kwargs(command, GuildCheck(halt_execution=halt_execution, error_message=error_message))
Only let a command run in a guild channel.
Parameters
- command (typing.Optional[CommandT]): The command to add this check to.
Other Parameters
error_message (typing.Optional[str]): The error message to send in response as a command error if the check fails.
Defaults to "Command can only be used in guild channels" and setting this to
Nonewill disable the error message allowing the command search to continue.halt_execution (bool): Whether this check should raise
tanjun.errors.HaltExecutionto end the execution search when it fails instead of returningFalse.Defaults to
False.
Notes
- error_message takes priority over halt_execution.
- For more information on how this is used with other parameters see
CallbackReturnT.
Returns
- CallbackReturnT[CommandT]: The command this check was added to.
View Source
def with_nsfw_check( command: typing.Optional[CommandT] = None, /, *, error_message: typing.Optional[str] = "Command can only be used in NSFW channels", halt_execution: bool = False, ) -> CallbackReturnT[CommandT]: """Only let a command run in a channel that's marked as nsfw. Parameters ---------- command : typing.Optional[CommandT] The command to add this check to. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "Command can only be used in NSFW channels" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. Notes ----- * error_message takes priority over halt_execution. * For more information on how this is used with other parameters see `CallbackReturnT`. Returns ------- CallbackReturnT[CommandT] The command this check was added to. """ return _optional_kwargs(command, NsfwCheck(halt_execution=halt_execution, error_message=error_message))
Only let a command run in a channel that's marked as nsfw.
Parameters
- command (typing.Optional[CommandT]): The command to add this check to.
Other Parameters
error_message (typing.Optional[str]): The error message to send in response as a command error if the check fails.
Defaults to "Command can only be used in NSFW channels" and setting this to
Nonewill disable the error message allowing the command search to continue.halt_execution (bool): Whether this check should raise
tanjun.errors.HaltExecutionto end the execution search when it fails instead of returningFalse.Defaults to
False.
Notes
- error_message takes priority over halt_execution.
- For more information on how this is used with other parameters see
CallbackReturnT.
Returns
- CallbackReturnT[CommandT]: The command this check was added to.
View Source
def with_sfw_check( command: typing.Optional[CommandT] = None, /, *, error_message: typing.Optional[str] = "Command can only be used in SFW channels", halt_execution: bool = False, ) -> CallbackReturnT[CommandT]: """Only let a command run in a channel that's marked as sfw. Parameters ---------- command : typing.Optional[CommandT] The command to add this check to. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "Command can only be used in SFW channels" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. Notes ----- * error_message takes priority over halt_execution. * For more information on how this is used with other parameters see `CallbackReturnT`. Returns ------- CallbackReturnT[CommandT] The command this check was added to. """ return _optional_kwargs(command, SfwCheck(halt_execution=halt_execution, error_message=error_message))
Only let a command run in a channel that's marked as sfw.
Parameters
- command (typing.Optional[CommandT]): The command to add this check to.
Other Parameters
error_message (typing.Optional[str]): The error message to send in response as a command error if the check fails.
Defaults to "Command can only be used in SFW channels" and setting this to
Nonewill disable the error message allowing the command search to continue.halt_execution (bool): Whether this check should raise
tanjun.errors.HaltExecutionto end the execution search when it fails instead of returningFalse.Defaults to
False.
Notes
- error_message takes priority over halt_execution.
- For more information on how this is used with other parameters see
CallbackReturnT.
Returns
- CallbackReturnT[CommandT]: The command this check was added to.
View Source
def with_owner_check( command: typing.Optional[CommandT] = None, /, *, error_message: typing.Optional[str] = "Only bot owners can use this command", halt_execution: bool = False, ) -> CallbackReturnT[CommandT]: """Only let a command run if it's being triggered by one of the bot's owners. Parameters ---------- command : typing.Optional[CommandT] The command to add this check to. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "Only bot owners can use this command" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. Notes ----- * error_message takes priority over halt_execution. * For more information on how this is used with other parameters see `CallbackReturnT`. Returns ------- CallbackReturnT[CommandT] The command this check was added to. """ return _optional_kwargs(command, OwnerCheck(halt_execution=halt_execution, error_message=error_message))
Only let a command run if it's being triggered by one of the bot's owners.
Parameters
- command (typing.Optional[CommandT]): The command to add this check to.
Other Parameters
error_message (typing.Optional[str]): The error message to send in response as a command error if the check fails.
Defaults to "Only bot owners can use this command" and setting this to
Nonewill disable the error message allowing the command search to continue.halt_execution (bool): Whether this check should raise
tanjun.errors.HaltExecutionto end the execution search when it fails instead of returningFalse.Defaults to
False.
Notes
- error_message takes priority over halt_execution.
- For more information on how this is used with other parameters see
CallbackReturnT.
Returns
- CallbackReturnT[CommandT]: The command this check was added to.
View Source
def with_own_permission_check( permissions: typing.Union[hikari.Permissions, int], *, error_message: typing.Optional[str] = "Bot doesn't have the permissions required to run this command", halt_execution: bool = False, ) -> collections.Callable[[CommandT], CommandT]: """Only let a command run if we have certain permissions in the current channel. Parameters ---------- permissions: typing.Union[hikari.permissions.Permissions, int] The permission(s) required for this command to run. Other Parameters ---------------- error_message : typing.Optional[str] The error message to send in response as a command error if the check fails. Defaults to "Bot doesn't have the permissions required to run this command" and setting this to `None` will disable the error message allowing the command search to continue. halt_execution : bool Whether this check should raise `tanjun.errors.HaltExecution` to end the execution search when it fails instead of returning `False`. Defaults to `False`. Notes ----- * error_message takes priority over halt_execution. * This will only pass for commands in DMs if `permissions` is valid for a DM context (e.g. can't have any moderation permissions) Returns ------- collections.abc.Callable[[CommandT], CommandT] A command decorator callback which adds the check. """ return lambda command: command.add_check( OwnPermissionCheck(permissions, halt_execution=halt_execution, error_message=error_message) )
Only let a command run if we have certain permissions in the current channel.
Parameters
- permissions (typing.Union[hikari.permissions.Permissions, int]): The permission(s) required for this command to run.
Other Parameters
error_message (typing.Optional[str]): The error message to send in response as a command error if the check fails.
Defaults to "Bot doesn't have the permissions required to run this command" and setting this to
Nonewill disable the error message allowing the command search to continue.halt_execution (bool): Whether this check should raise
tanjun.errors.HaltExecutionto end the execution search when it fails instead of returningFalse.Defaults to
False.
Notes
- error_message takes priority over halt_execution.
- This will only pass for commands in DMs if
permissionsis valid for a DM context (e.g. can't have any moderation permissions)
Returns
- collections.abc.Callable[[CommandT], CommandT]: A command decorator callback which adds the check.
View Source
# -*- coding: utf-8 -*- # cython: language_level=3 # BSD 3-Clause License # # Copyright (c) 2020-2022, Faster Speeding # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Standard Tanjun client.""" from __future__ import annotations __all__: list[str] = [ "as_loader", "as_unloader", "Client", "ClientCallbackNames", "MessageAcceptsEnum", "PrefixGetterSig", "PrefixGetterSigT", ] import asyncio import enum import functools import importlib import importlib.abc as importlib_abc import importlib.util as importlib_util import inspect import itertools import logging import pathlib import typing import warnings from collections import abc as collections import hikari from hikari import traits as hikari_traits from . import abc as tanjun_abc from . import checks from . import context from . import dependencies from . import errors from . import hooks from . import injecting from . import utilities if typing.TYPE_CHECKING: import types _ClientT = typing.TypeVar("_ClientT", bound="Client") class _MessageContextMakerProto(typing.Protocol): def __call__( self, client: tanjun_abc.Client, injection_client: injecting.InjectorClient, content: str, message: hikari.Message, *, command: typing.Optional[tanjun_abc.MessageCommand[typing.Any]] = None, component: typing.Optional[tanjun_abc.Component] = None, triggering_name: str = "", triggering_prefix: str = "", ) -> context.MessageContext: raise NotImplementedError class _SlashContextMakerProto(typing.Protocol): def __call__( self, client: tanjun_abc.Client, injection_client: injecting.InjectorClient, interaction: hikari.CommandInteraction, *, command: typing.Optional[tanjun_abc.BaseSlashCommand] = None, component: typing.Optional[tanjun_abc.Component] = None, default_to_ephemeral: bool = False, on_not_found: typing.Optional[ collections.Callable[[context.SlashContext], collections.Awaitable[None]] ] = None, ) -> context.SlashContext: raise NotImplementedError PrefixGetterSig = collections.Callable[..., collections.Awaitable[collections.Iterable[str]]] """Type hint of a callable used to get the prefix(es) for a specific guild. This should be an asynchronous callable which returns an iterable of strings. .. note:: While dependency injection is supported for this, the first positional argument will always be a `tanjun.abc.MessageContext`. """ PrefixGetterSigT = typing.TypeVar("PrefixGetterSigT", bound="PrefixGetterSig") _LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.tanjun.clients") class _LoaderDescriptor(tanjun_abc.ClientLoader): # Slots mess with functools.update_wrapper def __init__( self, callback: typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]], standard_impl: bool, ) -> None: self._callback = callback self._must_be_std = standard_impl functools.update_wrapper(self, callback) @property def has_load(self) -> bool: return True @property def has_unload(self) -> bool: return False def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> None: self._callback(*args, **kwargs) def load(self, client: tanjun_abc.Client, /) -> bool: if self._must_be_std: if not isinstance(client, Client): raise ValueError("This loader requires instances of the standard Client implementation") self._callback(client) else: typing.cast("collections.Callable[[tanjun_abc.Client], None]", self._callback)(client) return True def unload(self, _: tanjun_abc.Client, /) -> bool: return False class _UnloaderDescriptor(tanjun_abc.ClientLoader): # Slots mess with functools.update_wrapper def __init__( self, callback: typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]], standard_impl: bool, ) -> None: self._callback = callback self._must_be_std = standard_impl functools.update_wrapper(self, callback) @property def has_load(self) -> bool: return False @property def has_unload(self) -> bool: return True def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> None: self._callback(*args, **kwargs) def load(self, _: tanjun_abc.Client, /) -> bool: return False def unload(self, client: tanjun_abc.Client, /) -> bool: if self._must_be_std: if not isinstance(client, Client): raise ValueError("This unloader requires instances of the standard Client implementation") self._callback(client) else: typing.cast("collections.Callable[[tanjun_abc.Client], None]", self._callback)(client) return True @typing.overload def as_loader( callback: collections.Callable[[Client], None], /, *, standard_impl: typing.Literal[True] = True ) -> collections.Callable[[Client], None]: ... @typing.overload def as_loader( callback: collections.Callable[[tanjun_abc.Client], None], /, *, standard_impl: typing.Literal[False] ) -> collections.Callable[[tanjun_abc.Client], None]: ... def as_loader( callback: typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]], /, *, standard_impl: bool = True, ) -> typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]]: """Mark a callback as being used to load Tanjun components from a module. .. note:: This is only necessary if you wish to use `tanjun.Client.load_modules`. Parameters ---------- callback : collections.abc.Callable[[tanjun.abc.Client], None]] The callback used to load Tanjun components from a module. This should take one argument of type `Client` (or `tanjun.abc.Client` if `standard_impl` is `False`), return nothing and will be expected to initiate and add utilities such as components to the provided client. standard_impl : bool Whether this loader should only allow instances of `Client` as opposed to `tanjun.abc.Client`. Defaults to `True`. Returns ------- collections.abc.Callable[[tanjun.abc.Client], None]] The decorated load callback. """ return _LoaderDescriptor(callback, standard_impl) @typing.overload def as_unloader( callback: collections.Callable[[Client], None], /, *, standard_impl: typing.Literal[True] = True ) -> collections.Callable[[Client], None]: ... @typing.overload def as_unloader( callback: collections.Callable[[tanjun_abc.Client], None], /, *, standard_impl: typing.Literal[False] ) -> collections.Callable[[tanjun_abc.Client], None]: ... def as_unloader( callback: typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]], /, *, standard_impl: bool = True, ) -> typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]]: """Mark a callback as being used to unload a module's utilities from a client. .. note:: This is the inverse of `as_loader` and is only necessary if you wish to use the `tanjun.Client.unload_module` or `tanjun.Client.reload_module`. Parameters ---------- callback : collections.abc.Callable[[tanjun.Client], None]] The callback used to unload Tanjun components from a module. This should take one argument of type `Client` (or `tanjun.abc.Client` if `standard_impl` is `False`), return nothing and will be expected to remove utilities such as components from the provided client. standard_impl : bool Whether this unloader should only allow instances of `Client` as opposed to `tanjun.abc.Client`. Defaults to `True`. Returns ------- collections.abc.Callable[[tanjun.Client], None]] The decorated unload callback. """ return _UnloaderDescriptor(callback, standard_impl) ClientCallbackNames = tanjun_abc.ClientCallbackNames """Alias of `tanjun.abc.ClientCallbackNames`.""" class MessageAcceptsEnum(str, enum.Enum): """The possible configurations for which events `Client` should execute commands based on.""" ALL = "ALL" """Set the client to execute commands based on both DM and guild message create events.""" DM_ONLY = "DM_ONLY" """Set the client to execute commands based only DM message create events.""" GUILD_ONLY = "GUILD_ONLY" """Set the client to execute commands based only guild message create events.""" NONE = "NONE" """Set the client to not execute commands based on message create events.""" def get_event_type(self) -> typing.Optional[type[hikari.MessageCreateEvent]]: """Get the base event type this mode listens to. Returns ------- typing.Optional[type[hikari.message_events.MessageCreateEvent]] The type object of the MessageCreateEvent class this mode will register a listener for. This will be `None` if this mode disables listening to message create events. """ return _ACCEPTS_EVENT_TYPE_MAPPING[self] _ACCEPTS_EVENT_TYPE_MAPPING: dict[MessageAcceptsEnum, typing.Optional[type[hikari.MessageCreateEvent]]] = { MessageAcceptsEnum.ALL: hikari.MessageCreateEvent, MessageAcceptsEnum.DM_ONLY: hikari.DMMessageCreateEvent, MessageAcceptsEnum.GUILD_ONLY: hikari.GuildMessageCreateEvent, MessageAcceptsEnum.NONE: None, } def _check_human(ctx: tanjun_abc.Context, /) -> bool: return ctx.is_human async def _wrap_client_callback( callback: injecting.CallbackDescriptor[None], ctx: injecting.AbstractInjectionContext, args: tuple[str, ...], ) -> None: try: await callback.resolve(ctx, *args) except Exception as exc: _LOGGER.error("Client callback raised exception", exc_info=exc) async def on_parser_error(ctx: tanjun_abc.Context, error: errors.ParserError) -> None: """Handle message parser errors. This is the default message parser error hook included by `Client`. """ await ctx.respond(error.message) def _cmp_command(builder: typing.Optional[hikari.api.CommandBuilder], command: hikari.Command) -> bool: if not builder or builder.id is not hikari.UNDEFINED and builder.id != command.id: return False if builder.name != command.name or builder.description != command.description: return False default_perm = builder.default_permission if builder.default_permission is not hikari.UNDEFINED else True command_options = command.options or () if default_perm is not command.default_permission or len(builder.options) != len(command_options): return False return all(builder_option == option for builder_option, option in zip(builder.options, command_options)) class _StartDeclarer: __slots__ = ("client", "command_ids", "guild_id") def __init__( self, client: Client, command_ids: collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]], guild_id: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]], ) -> None: self.client = client self.command_ids = command_ids self.guild_id = guild_id async def __call__(self) -> None: try: await self.client.declare_global_commands(self.command_ids, guild=self.guild_id, force=False) finally: self.client.remove_client_callback(ClientCallbackNames.STARTING, self) class Client(injecting.InjectorClient, tanjun_abc.Client): """Tanjun's standard `tanjun.abc.Client` implementation. This implementation supports dependency injection for checks, command callbacks, prefix getters and event listeners. For more information on how this works see `tanjun.injecting`. .. note:: By default this client includes a parser error handling hook which will by overwritten if you call `Client.set_hooks`. """ __slots__ = ( "_accepts", "_auto_defer_after", "_cache", "_cached_application_id", "_checks", "_client_callbacks", "_components", "_defaults_to_ephemeral", "_make_message_context", "_make_slash_context", "_events", "_grab_mention_prefix", "_hooks", "_interaction_not_found", "_slash_hooks", "_is_closing", "_listeners", "_loop", "_message_hooks", "_metadata", "_modules", "_path_modules", "_prefix_getter", "_prefixes", "_rest", "_server", "_shards", "_voice", ) def __init__( self, rest: hikari.api.RESTClient, *, cache: typing.Optional[hikari.api.Cache] = None, events: typing.Optional[hikari.api.EventManager] = None, server: typing.Optional[hikari.api.InteractionServer] = None, shards: typing.Optional[hikari_traits.ShardAware] = None, voice: typing.Optional[hikari.api.VoiceComponent] = None, event_managed: bool = False, mention_prefix: bool = False, set_global_commands: typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] = False, declare_global_commands: typing.Union[ hikari.SnowflakeishSequence[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool ] = False, command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None, _stack_level: int = 0, ) -> None: """Initialise a Tanjun client. Notes ----- * For a quicker way to initiate this client around a standard bot aware client, see `Client.from_gateway_bot` and `Client.from_rest_bot`. * The endpoint used by `declare_global_commands` has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally). * `event_manager` is necessary for message command dispatch and will also be necessary for interaction command dispatch if `server` isn't provided. * `server` is used for interaction command dispatch if interaction events aren't being received from the event manager. Parameters ---------- rest : hikari.api.rest.RestClient The Hikari REST client this will use. Other Parameters ---------------- cache : hikari.api.cache.CacheClient The Hikari cache client this will use if applicable. event_manager : hikari.api.event_manager.EventManagerClient The Hikari event manager client this will use if applicable. server : hikari.api.interaction_server.InteractionServer The Hikari interaction server client this will use if applicable. shards : hikari.traits.ShardAware The Hikari shard aware client this will use if applicable. voice : hikari.api.voice.VoiceComponent The Hikari voice component this will use if applicable. event_managed : bool Whether or not this client is managed by the event manager. An event managed client will be automatically started and closed based on Hikari's lifetime events. Defaults to `False` and can only be passed as `True` if `event_manager` is also provided. mention_prefix : bool Whether or not mention prefixes should be automatically set when this client is first started. Defaults to `False` and it should be noted that this only applies to message commands. declare_global_commands : typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool] Whether or not to automatically set global slash commands when this client is first started. Defaults to `False`. If one or more guild objects/IDs are passed here then the registered global commands will be set on the specified guild(s) at startup rather than globally. This can be useful for testing/debug purposes as slash commands may take up to an hour to propagate globally but will immediately propagate when set on a specific guild. set_global_commands : typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] Deprecated as of v2.1.1a1 alias of `declare_global_commands`. command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] If provided, a mapping of top level command names to IDs of the commands to update. This field is complementary to `declare_global_commands` and, while it isn't necessarily required, this will in some situations help avoid permissions which were previously set for a command from being lost after a rename. This currently isn't supported when multiple guild IDs are passed for `declare_global_commands`. Raises ------ ValueError Raises for the following reasons: * If `event_managed` is `True` when `event_manager` is `None`. * If `command_ids` is passed when multiple guild ids are provided for `declare_global_commands`. * If `command_ids` is passed when `declare_global_commands` is `False`. """ # noqa: E501 - line too long # InjectorClient.__init__ super().__init__() if _LOGGER.isEnabledFor(logging.INFO): _LOGGER.info( "%s initialised with the following components: %s", "Event-managed client" if event_managed else "Client", ", ".join( name for name, value in [ ("cache", cache), ("event manager", events), ("interaction server", server), ("rest", rest), ("shard manager", shards), ] if value ), ) if not events and not server: _LOGGER.warning( "Client initiaited without an event manager or interaction server, " "automatic command dispatch will be unavailable." ) self._accepts = MessageAcceptsEnum.ALL if events else MessageAcceptsEnum.NONE self._auto_defer_after: typing.Optional[float] = 2.0 self._cache = cache self._cached_application_id: typing.Optional[hikari.Snowflake] = None self._checks: list[checks.InjectableCheck] = [] self._client_callbacks: dict[str, list[injecting.CallbackDescriptor[None]]] = {} self._components: dict[str, tanjun_abc.Component] = {} self._defaults_to_ephemeral: bool = False self._make_message_context: _MessageContextMakerProto = context.MessageContext self._make_slash_context: _SlashContextMakerProto = context.SlashContext self._events = events self._grab_mention_prefix = mention_prefix self._hooks: typing.Optional[tanjun_abc.AnyHooks] = hooks.AnyHooks().set_on_parser_error(on_parser_error) self._interaction_not_found: typing.Optional[str] = "Command not found" self._slash_hooks: typing.Optional[tanjun_abc.SlashHooks] = None self._is_closing = False self._listeners: dict[type[hikari.Event], list[injecting.SelfInjectingCallback[None]]] = {} self._loop: typing.Optional[asyncio.AbstractEventLoop] = None self._message_hooks: typing.Optional[tanjun_abc.MessageHooks] = None self._metadata: dict[typing.Any, typing.Any] = {} self._modules: dict[str, types.ModuleType] = {} self._path_modules: dict[pathlib.Path, types.ModuleType] = {} self._prefix_getter: typing.Optional[injecting.CallbackDescriptor[collections.Iterable[str]]] = None self._prefixes: list[str] = [] self._rest = rest self._server = server self._shards = shards self._voice = voice if event_managed: if not events: raise ValueError("Client cannot be event managed without an event manager") events.subscribe(hikari.StartingEvent, self._on_starting_event) events.subscribe(hikari.StoppingEvent, self._on_stopping_event) if set_global_commands: warnings.warn( "The `set_global_commands` argument is deprecated since v2.1.1a1. " "Use `declare_global_commands` instead.", DeprecationWarning, stacklevel=2 + _stack_level, ) declare_global_commands = declare_global_commands or set_global_commands command_ids = command_ids or {} if isinstance(declare_global_commands, collections.Sequence): if command_ids and len(declare_global_commands) > 1: raise ValueError( "Cannot provide specific command_ids while automatically " "declaring commands marked as 'global' in multiple-guilds on startup" ) for guild in declare_global_commands: _LOGGER.info("Registering startup command declarer for %s guild", guild) self.add_client_callback(ClientCallbackNames.STARTING, _StartDeclarer(self, command_ids, guild)) elif isinstance(declare_global_commands, bool): if declare_global_commands: _LOGGER.info("Registering startup command declarer for global commands") if not command_ids: _LOGGER.warning( "No command IDs passed for startup command declarer, this could lead to previously set " "command permissions being lost when commands are renamed." ) self.add_client_callback( ClientCallbackNames.STARTING, _StartDeclarer(self, command_ids, hikari.UNDEFINED) ) elif command_ids: raise ValueError("Cannot pass command IDs when not declaring global commands") else: self.add_client_callback( ClientCallbackNames.STARTING, _StartDeclarer(self, command_ids, declare_global_commands) ) ( self.set_type_dependency(tanjun_abc.Client, self) .set_type_dependency(Client, self) .set_type_dependency(type(self), self) .set_type_dependency(hikari.api.RESTClient, rest) .set_type_dependency(type(rest), rest) ) if cache: self.set_type_dependency(hikari.api.Cache, cache).set_type_dependency(type(cache), cache) if events: self.set_type_dependency(hikari.api.EventManager, events).set_type_dependency(type(events), events) if server: self.set_type_dependency(hikari.api.InteractionServer, server).set_type_dependency(type(server), server) if shards: self.set_type_dependency(hikari_traits.ShardAware, shards).set_type_dependency(type(shards), shards) if voice: self.set_type_dependency(hikari.api.VoiceComponent, voice).set_type_dependency(type(voice), voice) dependencies.set_standard_dependencies(self) @classmethod def from_gateway_bot( cls, bot: hikari_traits.GatewayBotAware, /, *, event_managed: bool = True, mention_prefix: bool = False, declare_global_commands: typing.Union[ hikari.SnowflakeishSequence[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool ] = False, set_global_commands: typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] = False, command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None, ) -> Client: """Build a `Client` from a `hikari.traits.GatewayBotAware` instance. Notes ----- * This implicitly defaults the client to human only mode. * This sets type dependency injectors for the hikari traits present in `bot` (including `hikari.traits.GatewayBotAware`). * The endpoint used by `declare_global_commands` has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally). Parameters ---------- bot : hikari.traits.GatewayBotAware The bot client to build from. This will be used to infer the relevant Hikari clients to use. Other Parameters ---------------- event_managed : bool Whether or not this client is managed by the event manager. An event managed client will be automatically started and closed based on Hikari's lifetime events. Defaults to `True`. mention_prefix : bool Whether or not mention prefixes should be automatically set when this client is first started. Defaults to `False` and it should be noted that this only applies to message commands. declare_global_commands : typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool] Whether or not to automatically set global slash commands when this client is first started. Defaults to `False`. If one or more guild objects/IDs are passed here then the registered global commands will be set on the specified guild(s) at startup rather than globally. This can be useful for testing/debug purposes as slash commands may take up to an hour to propagate globally but will immediately propagate when set on a specific guild. set_global_commands : typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] Deprecated as of v2.1.1a1 alias of `declare_global_commands`. command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] If provided, a mapping of top level command names to IDs of the commands to update. This field is complementary to `declare_global_commands` and, while it isn't necessarily required, this will in some situations help avoid permissions which were previously set for a command from being lost after a rename. This currently isn't supported when multiple guild IDs are passed for `declare_global_commands`. """ # noqa: E501 - line too long return ( cls( rest=bot.rest, cache=bot.cache, events=bot.event_manager, shards=bot, voice=bot.voice, event_managed=event_managed, mention_prefix=mention_prefix, declare_global_commands=declare_global_commands, set_global_commands=set_global_commands, command_ids=command_ids, _stack_level=1, ) .set_human_only() .set_hikari_trait_injectors(bot) ) @classmethod def from_rest_bot( cls, bot: hikari_traits.RESTBotAware, /, *, declare_global_commands: typing.Union[ hikari.SnowflakeishSequence[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool ] = False, set_global_commands: typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] = False, command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None, ) -> Client: """Build a `Client` from a `hikari.traits.RESTBotAware` instance. Notes ----- * This sets type dependency injectors for the hikari traits present in `bot` (including `hikari.traits.RESTBotAware`). * The endpoint used by `declare_global_commands` has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally). Parameters ---------- bot : hikari.traits.RESTBotAware The bot client to build from. Other Parameters ---------------- declare_global_commands : typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool] Whether or not to automatically set global slash commands when this client is first started. Defaults to `False`. If one or more guild objects/IDs are passed here then the registered global commands will be set on the specified guild(s) at startup rather than globally. This can be useful for testing/debug purposes as slash commands may take up to an hour to propagate globally but will immediately propagate when set on a specific guild. set_global_commands : typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] Deprecated as of v2.1.1a1 alias of `declare_global_commands`. command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] If provided, a mapping of top level command names to IDs of the commands to update. This field is complementary to `declare_global_commands` and, while it isn't necessarily required, this will in some situations help avoid permissions which were previously set for a command from being lost after a rename. This currently isn't supported when multiple guild IDs are passed for `declare_global_commands`. """ # noqa: E501 - line too long return cls( rest=bot.rest, server=bot.interaction_server, declare_global_commands=declare_global_commands, set_global_commands=set_global_commands, command_ids=command_ids, _stack_level=1, ).set_hikari_trait_injectors(bot) async def __aenter__(self) -> Client: await self.open() return self async def __aexit__( self, exc_type: typing.Optional[type[Exception]], exc: typing.Optional[Exception], exc_traceback: typing.Optional[types.TracebackType], ) -> None: await self.close() def __repr__(self) -> str: return f"CommandClient <{type(self).__name__!r}, {len(self._components)} components, {self._prefixes}>" @property def defaults_to_ephemeral(self) -> bool: # <<inherited docstring from tanjun.abc.Client>>. return self._defaults_to_ephemeral @property def message_accepts(self) -> MessageAcceptsEnum: """Type of message create events this command client accepts for execution.""" return self._accepts @property def is_human_only(self) -> bool: """Whether this client is only executing for non-bot/webhook users messages.""" return typing.cast("checks.InjectableCheck", _check_human) in self._checks @property def cache(self) -> typing.Optional[hikari.api.Cache]: # <<inherited docstring from tanjun.abc.Client>>. return self._cache @property def checks(self) -> collections.Collection[tanjun_abc.CheckSig]: """Collection of the level `tanjun.abc.Context` checks registered to this client. .. note:: These may be taking advantage of the standard dependency injection. """ return tuple(check.callback for check in self._checks) @property def components(self) -> collections.Collection[tanjun_abc.Component]: # <<inherited docstring from tanjun.abc.Client>>. return self._components.copy().values() @property def events(self) -> typing.Optional[hikari.api.EventManager]: # <<inherited docstring from tanjun.abc.Client>>. return self._events @property def listeners( self, ) -> collections.Mapping[type[hikari.Event], collections.Collection[tanjun_abc.ListenerCallbackSig]]: return utilities.CastedView( self._listeners, lambda x: [typing.cast(tanjun_abc.ListenerCallbackSig, callback.callback) for callback in x], ) @property def hooks(self) -> typing.Optional[tanjun_abc.AnyHooks]: """Top level `tanjun.abc.AnyHooks` set for this client. These are called during both message and interaction command execution. Returns ------- typing.Optional[tanjun.abc.AnyHooks] The top level `tanjun.abc.Context` based hooks set for this client if applicable, else `None`. """ return self._hooks @property def slash_hooks(self) -> typing.Optional[tanjun_abc.SlashHooks]: """Top level `tanjun.abc.SlashHooks` set for this client. These are only called during interaction command execution. """ return self._slash_hooks @property def is_alive(self) -> bool: # <<inherited docstring from tanjun.abc.Client>>. return self._loop is not None @property def loop(self) -> typing.Optional[asyncio.AbstractEventLoop]: # <<inherited docstring from tanjun.abc.Client>>. return self._loop @property def message_hooks(self) -> typing.Optional[tanjun_abc.MessageHooks]: """Top level `tanjun.abc.MessageHooks` set for this client. These are only called during both message command execution. """ return self._message_hooks @property def metadata(self) -> collections.MutableMapping[typing.Any, typing.Any]: # <<inherited docstring from tanjun.abc.Client>>. return self._metadata @property def prefix_getter(self) -> typing.Optional[PrefixGetterSig]: """Prefix getter method set for this client. For more information on this callback's signature see `PrefixGetter`. """ return typing.cast(PrefixGetterSig, self._prefix_getter.callback) if self._prefix_getter else None @property def prefixes(self) -> collections.Collection[str]: """Collection of the standard prefixes set for this client.""" return self._prefixes.copy() @property def rest(self) -> hikari.api.RESTClient: # <<inherited docstring from tanjun.abc.Client>>. return self._rest @property def server(self) -> typing.Optional[hikari.api.InteractionServer]: # <<inherited docstring from tanjun.abc.Client>>. return self._server @property def shards(self) -> typing.Optional[hikari_traits.ShardAware]: # <<inherited docstring from tanjun.abc.Client>>. return self._shards @property def voice(self) -> typing.Optional[hikari.api.VoiceComponent]: # <<inherited docstring from tanjun.abc.Client>>. return self._voice async def _on_starting_event(self, _: hikari.StartingEvent, /) -> None: await self.open() async def _on_stopping_event(self, _: hikari.StoppingEvent, /) -> None: await self.close() async def clear_application_commands( self, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, ) -> None: # <<inherited docstring from tanjun.abc.Client>>. if application is None: application = self._cached_application_id or await self.fetch_rest_application_id() await self._rest.set_application_commands(application, (), guild=guild) async def set_global_commands( self, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, force: bool = False, ) -> collections.Sequence[hikari.Command]: """Alias of `Client.declare_global_commands`. .. deprecated:: v2.1.1a1 Use `Client.declare_global_commands` instead. """ warnings.warn( "The `Client.set_global_commands` method has been deprecated since v2.1.1a1. " "Use `Client.declare_global_commands` instead.", DeprecationWarning, stacklevel=2, ) return await self.declare_global_commands(application=application, guild=guild, force=force) async def declare_global_commands( self, command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, force: bool = False, ) -> collections.Sequence[hikari.Command]: # <<inherited docstring from tanjun.abc.Client>>. commands = ( command for command in itertools.chain.from_iterable( component.slash_commands for component in self._components.values() ) if command.is_global ) return await self.declare_application_commands( commands, command_ids, application=application, guild=guild, force=force ) async def declare_application_command( self, command: tanjun_abc.BaseSlashCommand, /, command_id: typing.Optional[hikari.Snowflakeish] = None, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, ) -> hikari.Command: # <<inherited docstring from tanjun.abc.Client>>. builder = command.build() if command_id: response = await self._rest.edit_application_command( application or self._cached_application_id or await self.fetch_rest_application_id(), command_id, guild=guild, name=builder.name, description=builder.description, options=builder.options, ) else: response = await self._rest.create_application_command( application or self._cached_application_id or await self.fetch_rest_application_id(), guild=guild, name=builder.name, description=builder.description, options=builder.options, ) if not guild: command.set_tracked_command(response) # TODO: is this fine? return response async def declare_application_commands( self, commands: collections.Iterable[tanjun_abc.BaseSlashCommand], /, command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, force: bool = False, ) -> collections.Sequence[hikari.Command]: # <<inherited docstring from tanjun.abc.Client>>. command_ids = command_ids or {} names_to_commands: dict[str, tanjun_abc.BaseSlashCommand] = {} conflicts: set[str] = set() builders: dict[str, hikari.api.CommandBuilder] = {} for command in commands: names_to_commands[command.name] = command if command.name in builders: conflicts.add(command.name) builder = command.build() if command_id := command_ids.get(command.name): builder.set_id(hikari.Snowflake(command_id)) builders[command.name] = builder if conflicts: raise ValueError( "Couldn't declare commands due to conflicts. The following command names have more than one command " "registered for them " + ", ".join(conflicts) ) if len(builders) > 100: raise ValueError("You can only declare up to 100 top level commands in a guild or globally") if not application: application = self._cached_application_id or await self.fetch_rest_application_id() target_type = "global" if guild is hikari.UNDEFINED else f"guild {int(guild)}" if not force: registered_commands = await self._rest.fetch_application_commands(application, guild=guild) if len(registered_commands) == len(builders) and all( _cmp_command(builders.get(command.name), command) for command in registered_commands ): _LOGGER.info("Skipping bulk declare for %s slash commands since they're already declared", target_type) return registered_commands _LOGGER.info("Bulk declaring %s %s slash commands", len(builders), target_type) responses = await self._rest.set_application_commands(application, list(builders.values()), guild=guild) for response in responses: if not guild: names_to_commands[response.name].set_tracked_command(response) # TODO: is this fine? if (expected_id := command_ids.get(response.name)) and hikari.Snowflake(expected_id) != response.id: _LOGGER.warning( "ID mismatch found for %s command %r, expected %s but got %s. " "This suggests that any previous permissions set for this command will have been lost.", target_type, response.name, expected_id, response.id, ) _LOGGER.info("Successfully declared %s (top-level) %s commands", len(responses), target_type) if _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug( "Declared %s command ids; %s", target_type, ", ".join(f"{response.name}: {response.id}" for response in responses), ) return responses def set_auto_defer_after(self: _ClientT, time: typing.Optional[float], /) -> _ClientT: """Set when this client should automatically defer execution of commands. .. warning:: If `time` is set to `None` then automatic deferrals will be disabled. This may lead to unexpected behaviour. Parameters ---------- time : typing.Optional[float] The time in seconds to defer interaction command responses after. """ self._auto_defer_after = float(time) if time is not None else None return self def set_ephemeral_default(self: _ClientT, state: bool, /) -> _ClientT: """Set whether slash contexts spawned by this client should default to ephemeral responses. Parameters ---------- bool Whether slash command contexts executed in this component should should default to ephemeral. This will be overridden by any response calls which specify flags and defaults to `False`. Returns ------- SelfT This component to enable method chaining. """ self._defaults_to_ephemeral = state return self def set_hikari_trait_injectors(self: _ClientT, bot: hikari_traits.RESTAware, /) -> _ClientT: """Set type based dependency injection based on the hikari traits found in `bot`. This is a short hand for calling `Client.add_type_dependency` for all the hikari trait types `bot` is valid for with bot. Parameters ---------- bot : hikari_traits.RESTAware The hikari client to set dependency injectors for. """ for _, member in inspect.getmembers(hikari_traits): if inspect.isclass(member) and isinstance(bot, member): self.set_type_dependency(member, bot) return self def set_interaction_not_found(self: _ClientT, message: typing.Optional[str], /) -> _ClientT: """Set the response message for when an interaction command is not found. .. warning:: Setting this to `None` may lead to unexpected behaviour (especially when the client is still set to auto-defer interactions) and should only be done if you know what you're doing. Parameters ---------- message : typing.Optional[str] The message to respond with when an interaction command isn't found. """ self._interaction_not_found = message return self def set_message_accepts(self: _ClientT, accepts: MessageAcceptsEnum, /) -> _ClientT: """Set the kind of messages commands should be executed based on. Parameters ---------- accepts : MessageAcceptsEnum The type of messages commands should be executed based on. """ if accepts.get_event_type() and not self._events: raise ValueError("Cannot set accepts level on a client with no event manager") self._accepts = accepts return self def set_message_ctx_maker(self: _ClientT, maker: _MessageContextMakerProto = context.MessageContext, /) -> _ClientT: """Set the message context maker to use when creating context for a message. .. warning:: The caller must return an instance of `tanjun.context.MessageContext` rather than just any implementation of the MessageContext abc due to this client relying on implementation detail of `tanjun.context.MessageContext`. Parameters ---------- maker : _MessageContextMakerProto The message context maker to use. This is a callback which should match the signature of `tanjun.context.MessageContext.__init__` and return an instance of `tanjun.context.MessageContext`. This defaults to `tanjun.context.MessageContext`. """ self._make_message_context = maker return self def set_metadata(self: _ClientT, key: typing.Any, value: typing.Any, /) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. self._metadata[key] = value return self def set_slash_ctx_maker(self: _ClientT, maker: _SlashContextMakerProto = context.SlashContext, /) -> _ClientT: """Set the slash context maker to use when creating context for a slash command. .. warning:: The caller must return an instance of `tanjun.context.SlashContext` rather than just any implementation of the SlashContext abc due to this client relying on implementation detail of `tanjun.context.SlashContext`. Parameters ---------- maker : _SlashContextMakerProto The slash context maker to use. This is a callback which should match the signature of `tanjun.context.SlashContext.__init__` and return an instance of `tanjun.context.SlashContext`. This defaults to `tanjun.context.SlashContext`. """ self._make_slash_context = maker return self def set_human_only(self: _ClientT, value: bool = True) -> _ClientT: """Set whether or not message commands execution should be limited to "human" users. .. note:: This doesn't apply to interaction commands as these can only be triggered by a "human" (normal user account). Parameters ---------- value : bool Whether or not message commands execution should be limited to "human" users. Passing `True` here will prevent message commands from being executed based on webhook and bot messages. """ if value: self.add_check(_check_human) else: try: self.remove_check(_check_human) except ValueError: pass return self def add_check(self: _ClientT, check: tanjun_abc.CheckSig, /) -> _ClientT: """Add a generic check to this client. This will be applied to both message and slash command execution. Parameters ---------- check : tanjun_abc.CheckSig The check to add. This may be either synchronous or asynchronous and must take one positional argument of type `tanjun.abc.Context` with dependency injection being supported for its keyword arguments. Returns ------- Self The client instance to enable chained calls. """ if check not in self._checks: self._checks.append(checks.InjectableCheck(check)) return self def remove_check(self: _ClientT, check: tanjun_abc.CheckSig, /) -> _ClientT: """Remove a check from the client. Parameters ---------- check : tanjun_abc.CheckSig The check to remove. Raises ------ ValueError If the check was not previously added. """ self._checks.remove(typing.cast("checks.InjectableCheck", check)) return self def with_check(self, check: tanjun_abc.CheckSigT, /) -> tanjun_abc.CheckSigT: """Add a check to this client through a decorator call. Parameters ---------- check : tanjun_abc.CheckSig The check to add. This may be either synchronous or asynchronous and must take one positional argument of type `tanjun.abc.Context` with dependency injection being supported for its keyword arguments. Returns ------- tanjun_abc.CheckSig The added check. """ self.add_check(check) return check async def check(self, ctx: tanjun_abc.Context, /) -> bool: return await utilities.gather_checks(ctx, self._checks) def add_component(self: _ClientT, component: tanjun_abc.Component, /, *, add_injector: bool = False) -> _ClientT: """Add a component to this client. Parameters ---------- component: Component The component to move to this client. Returns ------- Self The client instance to allow chained calls. Raises ------ ValueError If the component's name is already registered. """ if component.name in self._components: raise ValueError(f"A component named {component.name!r} is already registered.") component.bind_client(self) self._components[component.name] = component if add_injector: self.set_type_dependency(type(component), lambda: component) if self._loop: self._loop.create_task(component.open()) self._loop.create_task(self.dispatch_client_callback(ClientCallbackNames.COMPONENT_ADDED, component)) return self def get_component_by_name(self, name: str, /) -> typing.Optional[tanjun_abc.Component]: # <<inherited docstring from tanjun.abc.Client>>. return self._components.get(name) def remove_component(self: _ClientT, component: tanjun_abc.Component, /) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. stored_component = self._components.get(component.name) if not stored_component or stored_component != component: raise ValueError(f"The component {component!r} is not registered.") del self._components[component.name] if self._loop: self._loop.create_task(component.close(unbind=True)) self._loop.create_task( self.dispatch_client_callback(ClientCallbackNames.COMPONENT_REMOVED, stored_component) ) else: stored_component.unbind_client(self) return self def remove_component_by_name(self: _ClientT, name: str, /) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. return self.remove_component(self._components[name]) def add_client_callback( self: _ClientT, name: typing.Union[str, tanjun_abc.ClientCallbackNames], callback: tanjun_abc.MetaEventSig, / ) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. descriptor = injecting.CallbackDescriptor(callback) name = name.casefold() try: if descriptor in self._client_callbacks[name]: return self self._client_callbacks[name].append(descriptor) except KeyError: self._client_callbacks[name] = [descriptor] return self async def dispatch_client_callback( self, name: typing.Union[str, tanjun_abc.ClientCallbackNames], /, *args: typing.Any ) -> None: # <<inherited docstring from tanjun.abc.Client>>. name = name.casefold() if callbacks := self._client_callbacks.get(name): calls = ( _wrap_client_callback(callback, injecting.BasicInjectionContext(self), args) for callback in callbacks ) await asyncio.gather(*calls) def get_client_callbacks( self, name: typing.Union[str, tanjun_abc.ClientCallbackNames], / ) -> collections.Collection[tanjun_abc.MetaEventSig]: # <<inherited docstring from tanjun.abc.Client>>. name = name.casefold() if result := self._client_callbacks.get(name): return tuple(callback.callback for callback in result) return () def remove_client_callback( self: _ClientT, name: typing.Union[str, tanjun_abc.ClientCallbackNames], callback: tanjun_abc.MetaEventSig, / ) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. name = name.casefold() self._client_callbacks[name].remove(typing.cast("injecting.CallbackDescriptor[None]", callback)) if not self._client_callbacks[name]: del self._client_callbacks[name] return self def with_client_callback( self, name: typing.Union[str, tanjun_abc.ClientCallbackNames], / ) -> collections.Callable[[tanjun_abc.MetaEventSigT], tanjun_abc.MetaEventSigT]: # <<inherited docstring from tanjun.abc.Client>>. def decorator(callback: tanjun_abc.MetaEventSigT, /) -> tanjun_abc.MetaEventSigT: self.add_client_callback(name, callback) return callback return decorator def add_listener( self: _ClientT, event_type: type[hikari.Event], callback: tanjun_abc.ListenerCallbackSig, / ) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. injected: injecting.SelfInjectingCallback[None] = injecting.SelfInjectingCallback(self, callback) try: if callback in self._listeners[event_type]: return self self._listeners[event_type].append(injected) except KeyError: self._listeners[event_type] = [injected] if self._loop and self._events: self._events.subscribe(event_type, injected.__call__) return self def remove_listener( self: _ClientT, event_type: type[hikari.Event], callback: tanjun_abc.ListenerCallbackSig, / ) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. index = self._listeners[event_type].index(typing.cast("injecting.SelfInjectingCallback[None]", callback)) registered_callback = self._listeners[event_type].pop(index) if not self._listeners[event_type]: del self._listeners[event_type] if self._loop and self._events: self._events.unsubscribe(event_type, registered_callback.__call__) return self def with_listener( self, event_type: type[hikari.Event], / ) -> collections.Callable[[tanjun_abc.ListenerCallbackSigT], tanjun_abc.ListenerCallbackSigT]: # <<inherited docstring from tanjun.abc.Client>>. def decorator(callback: tanjun_abc.ListenerCallbackSigT, /) -> tanjun_abc.ListenerCallbackSigT: self.add_listener(event_type, callback) return callback return decorator def add_prefix(self: _ClientT, prefixes: typing.Union[collections.Iterable[str], str], /) -> _ClientT: """Add a prefix used to filter message command calls. This will be matched against the first character(s) in a message's content to determine whether the message command search stage of execution should be initiated. Parameters ---------- prefixes : typing.Union[collections.abc.Iterable[str], str] Either a single string or an iterable of strings to be used as prefixes. Returns ------- Self The client instance to enable chained calls. """ if isinstance(prefixes, str): if prefixes not in self._prefixes: self._prefixes.append(prefixes) else: self._prefixes.extend(prefix for prefix in prefixes if prefix not in self._prefixes) return self def remove_prefix(self: _ClientT, prefix: str, /) -> _ClientT: """Remove a message content prefix from the client. Parameters ---------- prefix : str The prefix to remove. Raises ------ ValueError If the prefix is not registered with the client. Returns ------- Self The client instance to enable chained calls. """ self._prefixes.remove(prefix) return self def set_prefix_getter(self: _ClientT, getter: typing.Optional[PrefixGetterSig], /) -> _ClientT: """Set the callback used to retrieve message prefixes set for the relevant guild. Parameters ---------- getter : typing.Optional[PrefixGetterSig] The callback which'll be used to retrieve prefixes for the guild a message context is from. If `None` is passed here then the callback will be unset. This should be an async callback which one argument of type `tanjun.abc.MessageContext` and returns an iterable of string prefixes. Dependency injection is supported for this callback's keyword arguments. Returns ------- Self The client instance to enable chained calls. """ self._prefix_getter = injecting.CallbackDescriptor(getter) if getter else None return self def with_prefix_getter(self, getter: PrefixGetterSigT, /) -> PrefixGetterSigT: """Set the prefix getter callback for this client through decorator call. Examples -------- ```py client = tanjun.Client.from_rest_bot(bot) @client.with_prefix_getter async def prefix_getter(ctx: tanjun.abc.MessageContext) -> collections.abc.Iterable[str]: raise NotImplementedError ``` Parameters ---------- getter : PrefixGetterSig The callback which'll be to retrieve prefixes for the guild a message event is from. This should be an async callback which one argument of type `tanjun.abc.MessageContext` and returns an iterable of string prefixes. Dependency injection is supported for this callback's keyword arguments. Returns ------- PrefixGetterSigT The registered callback. """ self.set_prefix_getter(getter) return getter def iter_commands(self) -> collections.Iterator[tanjun_abc.ExecutableCommand[tanjun_abc.Context]]: # <<inherited docstring from tanjun.abc.Client>>. slash_commands = self.iter_slash_commands(global_only=False) yield from self.iter_message_commands() yield from slash_commands def iter_message_commands(self) -> collections.Iterator[tanjun_abc.MessageCommand[typing.Any]]: # <<inherited docstring from tanjun.abc.Client>>. return itertools.chain.from_iterable(component.message_commands for component in self.components) def iter_slash_commands(self, *, global_only: bool = False) -> collections.Iterator[tanjun_abc.BaseSlashCommand]: # <<inherited docstring from tanjun.abc.Client>>. if global_only: return filter(lambda c: c.is_global, self.iter_slash_commands(global_only=False)) return itertools.chain.from_iterable(component.slash_commands for component in self.components) def check_message_name( self, name: str, / ) -> collections.Iterator[tuple[str, tanjun_abc.MessageCommand[typing.Any]]]: # <<inherited docstring from tanjun.abc.Client>>. return itertools.chain.from_iterable( component.check_message_name(name) for component in self._components.values() ) def check_slash_name(self, name: str, /) -> collections.Iterator[tanjun_abc.BaseSlashCommand]: # <<inherited docstring from tanjun.abc.Client>>. return itertools.chain.from_iterable( component.check_slash_name(name) for component in self._components.values() ) async def _check_prefix(self, ctx: tanjun_abc.MessageContext, /) -> typing.Optional[str]: if self._prefix_getter: for prefix in await self._prefix_getter.resolve_with_command_context(ctx, ctx): if ctx.content.startswith(prefix): return prefix for prefix in self._prefixes: if ctx.content.startswith(prefix): return prefix return None def _try_unsubscribe( self, event_manager: hikari.api.EventManager, event_type: type[hikari.Event], callback: tanjun_abc.ListenerCallbackSig, ) -> None: try: event_manager.unsubscribe(event_type, callback) except (ValueError, LookupError): # TODO: add logging here pass async def close(self, *, deregister_listeners: bool = True) -> None: """Close the client. Raises ------ RuntimeError If the client isn't running. """ if not self._loop: raise RuntimeError("Client isn't active") if self._is_closing: event = asyncio.Event() self.add_client_callback(ClientCallbackNames.CLOSED, event.set) try: await event.wait() finally: self.remove_client_callback(ClientCallbackNames.CLOSED, event.set) return self._is_closing = True await self.dispatch_client_callback(ClientCallbackNames.CLOSING) if deregister_listeners and self._events: if event_type := self._accepts.get_event_type(): self._try_unsubscribe(self._events, event_type, self.on_message_create_event) self._try_unsubscribe(self._events, hikari.InteractionCreateEvent, self.on_interaction_create_event) for event_type_, listeners in self._listeners.items(): for listener in listeners: self._try_unsubscribe(self._events, event_type_, listener.__call__) if deregister_listeners and self._server: self._server.set_listener(hikari.CommandInteraction, None) await asyncio.gather(*(component.close() for component in self._components.copy().values())) self._loop = None await self.dispatch_client_callback(ClientCallbackNames.CLOSED) self._is_closing = False async def open(self, *, register_listeners: bool = True) -> None: """Start the client. If `mention_prefix` was passed to `Client.__init__` or `Client.from_gateway_bot` then this function may make a fetch request to Discord if it cannot get the current user from the cache. Raises ------ RuntimeError If the client is already active. """ if self._loop: raise RuntimeError("Client is already alive") self._loop = asyncio.get_running_loop() self._is_closing = False await self.dispatch_client_callback(ClientCallbackNames.STARTING) if self._grab_mention_prefix: user: typing.Optional[hikari.OwnUser] = None if self._cache: user = self._cache.get_me() if not user and (user_cache := self.get_type_dependency(dependencies.SingleStoreCache[hikari.OwnUser])): user = await user_cache.get(default=None) if not user: user = await self._rest.fetch_my_user() for prefix in f"<@{user.id}>", f"<@!{user.id}>": if prefix not in self._prefixes: self._prefixes.append(prefix) self._grab_mention_prefix = False await asyncio.gather(*(component.open() for component in self._components.copy().values())) if register_listeners and self._events: if event_type := self._accepts.get_event_type(): self._events.subscribe(event_type, self.on_message_create_event) self._events.subscribe(hikari.InteractionCreateEvent, self.on_interaction_create_event) for event_type_, listeners in self._listeners.items(): for listener in listeners: self._events.subscribe(event_type_, listener.__call__) if register_listeners and self._server: self._server.set_listener(hikari.CommandInteraction, self.on_interaction_create_request) self._loop.create_task(self.dispatch_client_callback(ClientCallbackNames.STARTED)) async def fetch_rest_application_id(self) -> hikari.Snowflake: """Fetch the ID of the application this client is linked to. Returns ------- hikari.Snowflake The application ID of the application this client is linked to. """ if self._cached_application_id: return self._cached_application_id application_cache = self.get_type_dependency( dependencies.SingleStoreCache[hikari.Application] ) or self.get_type_dependency(dependencies.SingleStoreCache[hikari.AuthorizationApplication]) if application_cache and (application := await application_cache.get(default=None)): self._cached_application_id = application.id return application.id if self._rest.token_type == hikari.TokenType.BOT: self._cached_application_id = hikari.Snowflake(await self._rest.fetch_application()) else: self._cached_application_id = hikari.Snowflake((await self._rest.fetch_authorization()).application) return self._cached_application_id def set_hooks(self: _ClientT, hooks: typing.Optional[tanjun_abc.AnyHooks], /) -> _ClientT: """Set the general command execution hooks for this client. The callbacks within this hook will be added to every slash and message command execution started by this client. Parameters ---------- hooks : typing.Optional[tanjun_abc.AnyHooks] The general command execution hooks to set for this client. Passing `None` will remove all hooks. Returns ------- Self The client instance to enable chained calls. """ self._hooks = hooks return self def set_slash_hooks(self: _ClientT, hooks: typing.Optional[tanjun_abc.SlashHooks], /) -> _ClientT: """Set the slash command execution hooks for this client. The callbacks within this hook will be added to every slash command execution started by this client. Parameters ---------- hooks : typing.Optional[tanjun_abc.SlashHooks] The slash context specific command execution hooks to set for this client. Passing `None` will remove the hooks. Returns ------- Self The client instance to enable chained calls. """ self._slash_hooks = hooks return self def set_message_hooks(self: _ClientT, hooks: typing.Optional[tanjun_abc.MessageHooks], /) -> _ClientT: """Set the message command execution hooks for this client. The callbacks within this hook will be added to every message command execution started by this client. Parameters ---------- hooks : typing.Optional[tanjun_abc.MessageHooks] The message context specific command execution hooks to set for this client. Passing `None` will remove all hooks. Returns ------- Self The client instance to enable chained calls. """ self._message_hooks = hooks return self def _call_loaders( self, module_path: typing.Union[str, pathlib.Path], loaders: list[tanjun_abc.ClientLoader], / ) -> None: found = False for loader in loaders: if loader.load(self): found = True if not found: raise errors.ModuleMissingLoaders(f"Didn't find any loaders in {module_path}", module_path) def _call_unloaders( self, module_path: typing.Union[str, pathlib.Path], loaders: list[tanjun_abc.ClientLoader], / ) -> None: found = False for loader in loaders: if loader.unload(self): found = True if not found: raise errors.ModuleMissingLoaders(f"Didn't find any unloaders in {module_path}", module_path) def _load_module( self, module_path: typing.Union[str, pathlib.Path] ) -> collections.Generator[collections.Callable[[], types.ModuleType], types.ModuleType, None]: if isinstance(module_path, str): if module_path in self._modules: raise errors.ModuleStateConflict(f"module {module_path} already loaded", module_path) _LOGGER.info("Loading from %s", module_path) module = yield lambda: importlib.import_module(module_path) with _WrapLoadError(errors.FailedModuleLoad): self._call_loaders(module_path, _get_loaders(module, module_path)) self._modules[module_path] = module else: module_path_abs = module_path.absolute() if module_path_abs in self._path_modules: raise errors.ModuleStateConflict(f"Module at {module_path} already loaded", module_path) _LOGGER.info("Loading from %s", module_path) module = yield lambda: _get_path_module(module_path) with _WrapLoadError(errors.FailedModuleLoad): self._call_loaders(module_path, _get_loaders(module, module_path)) self._path_modules[module_path_abs] = module def load_modules(self: _ClientT, *modules: typing.Union[str, pathlib.Path]) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. for module_path in modules: if isinstance(module_path, pathlib.Path): module_path = module_path.absolute() generator = self._load_module(module_path) load_module = next(generator) with _WrapLoadError(errors.FailedModuleLoad): module = load_module() try: generator.send(module) except StopIteration: pass else: raise RuntimeError("Generator didn't finish") return self async def load_modules_async(self, *modules: typing.Union[str, pathlib.Path]) -> None: # <<inherited docstring from tanjun.abc.Client>>. loop = asyncio.get_running_loop() for module_path in modules: if isinstance(module_path, pathlib.Path): module_path = await loop.run_in_executor(None, module_path.absolute) generator = self._load_module(module_path) load_module = next(generator) with _WrapLoadError(errors.FailedModuleLoad): module = await loop.run_in_executor(None, load_module) try: generator.send(module) except StopIteration: pass else: raise RuntimeError("Generator didn't finish") def unload_modules(self: _ClientT, *modules: typing.Union[str, pathlib.Path]) -> _ClientT: # <<inherited docstring from tanjun.ab.Client>>. for module_path in modules: if isinstance(module_path, str): modules_dict: dict[typing.Any, types.ModuleType] = self._modules else: modules_dict = self._path_modules module_path = module_path.absolute() module = modules_dict.get(module_path) if not module: raise errors.ModuleStateConflict(f"Module {module_path!s} not loaded", module_path) _LOGGER.info("Unloading from %s", module_path) with _WrapLoadError(errors.FailedModuleUnload): self._call_unloaders(module_path, _get_loaders(module, module_path)) del modules_dict[module_path] return self def _reload_module( self, module_path: typing.Union[str, pathlib.Path] ) -> collections.Generator[collections.Callable[[], types.ModuleType], types.ModuleType, None]: if isinstance(module_path, str): old_module = self._modules.get(module_path) def load_module() -> types.ModuleType: assert old_module return importlib.reload(old_module) modules_dict: dict[typing.Any, types.ModuleType] = self._modules else: old_module = self._path_modules.get(module_path) def load_module() -> types.ModuleType: assert isinstance(module_path, pathlib.Path) return _get_path_module(module_path) modules_dict = self._path_modules if not old_module: raise errors.ModuleStateConflict(f"Module {module_path} not loaded", module_path) _LOGGER.info("Reloading %s", module_path) old_loaders = _get_loaders(old_module, module_path) # We assert that the old module has unloaders early to avoid unnecessarily # importing the new module. if not any(loader.has_unload for loader in old_loaders): raise errors.ModuleMissingLoaders(f"Didn't find any unloaders in old {module_path}", module_path) module = yield load_module loaders = _get_loaders(module, module_path) # We assert that the new module has loaders early to avoid unnecessarily # unloading then rolling back when we know it's going to fail to load. if not any(loader.has_load for loader in loaders): raise errors.ModuleMissingLoaders(f"Didn't find any loaders in new {module_path}", module_path) with _WrapLoadError(errors.FailedModuleUnload): # This will never raise MissingLoaders as we assert this earlier self._call_unloaders(module_path, old_loaders) try: # This will never raise MissingLoaders as we assert this earlier self._call_loaders(module_path, loaders) except Exception as exc: self._call_loaders(module_path, old_loaders) raise errors.FailedModuleLoad from exc else: modules_dict[module_path] = module def reload_modules(self: _ClientT, *modules: typing.Union[str, pathlib.Path]) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. for module_path in modules: if isinstance(module_path, pathlib.Path): module_path = module_path.absolute() generator = self._reload_module(module_path) load_module = next(generator) with _WrapLoadError(errors.FailedModuleLoad): module = load_module() try: generator.send(module) except StopIteration: pass else: raise RuntimeError("Generator didn't finish") return self async def reload_modules_async(self, *modules: typing.Union[str, pathlib.Path]) -> None: # <<inherited docstring from tanjun.abc.Client>>. loop = asyncio.get_running_loop() for module_path in modules: if isinstance(module_path, pathlib.Path): module_path = await loop.run_in_executor(None, module_path.absolute) generator = self._reload_module(module_path) load_module = next(generator) with _WrapLoadError(errors.FailedModuleLoad): module = await loop.run_in_executor(None, load_module) try: generator.send(module) except StopIteration: pass else: raise RuntimeError("Generator didn't finish") async def on_message_create_event(self, event: hikari.MessageCreateEvent, /) -> None: """Execute a message command based on a gateway event. Parameters ---------- hikari.events.message_events.MessageCreateEvent The event to handle. """ if event.message.content is None: return ctx = self._make_message_context( client=self, injection_client=self, content=event.message.content, message=event.message ) if (prefix := await self._check_prefix(ctx)) is None: return ctx.set_content(ctx.content.lstrip()[len(prefix) :].lstrip()).set_triggering_prefix(prefix) hooks: typing.Optional[set[tanjun_abc.MessageHooks]] = None if self._hooks and self._message_hooks: hooks = {self._hooks, self._message_hooks} elif self._hooks: hooks = {self._hooks} elif self._message_hooks: hooks = {self._message_hooks} try: if await self.check(ctx): for component in self._components.values(): if await component.execute_message(ctx, hooks=hooks): return except errors.HaltExecution: pass except errors.CommandError as exc: await ctx.respond(exc.message) return await self.dispatch_client_callback(ClientCallbackNames.MESSAGE_COMMAND_NOT_FOUND, ctx) def _get_slash_hooks(self) -> typing.Optional[set[tanjun_abc.SlashHooks]]: hooks: typing.Optional[set[tanjun_abc.SlashHooks]] = None if self._hooks and self._slash_hooks: hooks = {self._hooks, self._slash_hooks} elif self._hooks: hooks = {self._hooks} elif self._slash_hooks: hooks = {self._slash_hooks} return hooks async def _on_slash_not_found(self, ctx: context.SlashContext) -> None: await self.dispatch_client_callback(ClientCallbackNames.SLASH_COMMAND_NOT_FOUND, ctx) if self._interaction_not_found and not ctx.has_responded: await ctx.create_initial_response(self._interaction_not_found) async def on_interaction_create_event(self, event: hikari.InteractionCreateEvent, /) -> None: """Execute a slash command based on Gateway events. .. note:: Any event where `event.interaction` is not `hikari.CommandInteraction` will be ignored. Parameters ---------- event : hikari.events.interaction_events.InteractionCreateEvent The event to execute commands based on. """ if not isinstance(event.interaction, hikari.CommandInteraction): return ctx = self._make_slash_context( client=self, injection_client=self, interaction=event.interaction, on_not_found=self._on_slash_not_found, default_to_ephemeral=self._defaults_to_ephemeral, ) hooks = self._get_slash_hooks() if self._auto_defer_after is not None: ctx.start_defer_timer(self._auto_defer_after) try: if await self.check(ctx): for component in self._components.values(): # This is set on each iteration to ensure that any component # state which was set to this isn't propagated to other components. ctx.set_ephemeral_default(self._defaults_to_ephemeral) if future := await component.execute_interaction(ctx, hooks=hooks): await future return except errors.HaltExecution: pass except errors.CommandError as exc: await ctx.respond(exc.message) return await ctx.mark_not_found() async def on_interaction_create_request(self, interaction: hikari.CommandInteraction, /) -> context.ResponseTypeT: """Execute a slash command based on received REST requests. Parameters ---------- interaction : hikari.CommandInteraction The interaction to execute a command based on. Returns ------- tanjun.context.ResponseType The initial response to send back to Discord. """ ctx = self._make_slash_context( client=self, injection_client=self, interaction=interaction, on_not_found=self._on_slash_not_found, default_to_ephemeral=self._defaults_to_ephemeral, ) if self._auto_defer_after is not None: ctx.start_defer_timer(self._auto_defer_after) hooks = self._get_slash_hooks() future = ctx.get_response_future() try: if await self.check(ctx): for component in self._components.values(): # This is set on each iteration to ensure that any component # state which was set to this isn't propagated to other components. ctx.set_ephemeral_default(self._defaults_to_ephemeral) if await component.execute_interaction(ctx, hooks=hooks): return await future except errors.HaltExecution: pass except errors.CommandError as exc: # Under very specific timing there may be another future which could set a result while we await # ctx.respond therefore we create a task to avoid any erroneous behaviour from this trying to create # another response before it's returned the initial response. asyncio.get_running_loop().create_task( ctx.respond(exc.message), name=f"{interaction.id} command error responder" ) return await future asyncio.get_running_loop().create_task(ctx.mark_not_found(), name=f"{interaction.id} not found") return await future def _get_loaders( module: types.ModuleType, module_path: typing.Union[str, pathlib.Path], / ) -> list[tanjun_abc.ClientLoader]: exported = getattr(module, "__all__", None) if exported is not None and isinstance(exported, collections.Iterable): _LOGGER.debug("Scanning %s module based on its declared __all__)", module_path) exported = typing.cast("collections.Iterable[typing.Any]", exported) iterator = (getattr(module, name, None) for name in exported if isinstance(name, str)) else: _LOGGER.debug("Scanning all public members on %s", module_path) iterator = ( member for name, member in inspect.getmembers(module) if not name.startswith("_") or name.startswith("__") and name.endswith("__") ) return [value for value in iterator if isinstance(value, tanjun_abc.ClientLoader)] def _get_path_module(module_path: pathlib.Path, /) -> types.ModuleType: module_name = module_path.name.rsplit(".", 1)[0] spec = importlib_util.spec_from_file_location(module_name, module_path) # https://github.com/python/typeshed/issues/2793 if not spec or not isinstance(spec.loader, importlib_abc.Loader): raise ModuleNotFoundError(f"Module not found at {module_path}", name=module_name, path=str(module_path)) module = importlib_util.module_from_spec(spec) spec.loader.exec_module(module) return module class _WrapLoadError: __slots__ = ("_error",) def __init__(self, error: collections.Callable[[], Exception], /) -> None: self._error = error def __enter__(self) -> None: pass def __exit__( self, exc_type: typing.Optional[type[Exception]], exc: typing.Optional[Exception], exc_tb: typing.Optional[types.TracebackType], ) -> None: if exc and not isinstance(exc, errors.ModuleMissingLoaders): raise self._error() from exc # noqa: R102 unnecessary parenthesis on raised exception
Standard Tanjun client.
View Source
def as_loader( callback: typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]], /, *, standard_impl: bool = True, ) -> typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]]: """Mark a callback as being used to load Tanjun components from a module. .. note:: This is only necessary if you wish to use `tanjun.Client.load_modules`. Parameters ---------- callback : collections.abc.Callable[[tanjun.abc.Client], None]] The callback used to load Tanjun components from a module. This should take one argument of type `Client` (or `tanjun.abc.Client` if `standard_impl` is `False`), return nothing and will be expected to initiate and add utilities such as components to the provided client. standard_impl : bool Whether this loader should only allow instances of `Client` as opposed to `tanjun.abc.Client`. Defaults to `True`. Returns ------- collections.abc.Callable[[tanjun.abc.Client], None]] The decorated load callback. """ return _LoaderDescriptor(callback, standard_impl)
Mark a callback as being used to load Tanjun components from a module.
Note:
This is only necessary if you wish to use tanjun.Client.load_modules.
Parameters
callback (collections.abc.Callable[[tanjun.abc.Client], None]]): The callback used to load Tanjun components from a module.
This should take one argument of type
Client(ortanjun.abc.Clientifstandard_implisFalse), return nothing and will be expected to initiate and add utilities such as components to the provided client.standard_impl (bool): Whether this loader should only allow instances of
Clientas opposed totanjun.abc.Client.Defaults to
True.
Returns
- collections.abc.Callable[[tanjun.abc.Client], None]]: The decorated load callback.
View Source
def as_unloader( callback: typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]], /, *, standard_impl: bool = True, ) -> typing.Union[collections.Callable[[Client], None], collections.Callable[[tanjun_abc.Client], None]]: """Mark a callback as being used to unload a module's utilities from a client. .. note:: This is the inverse of `as_loader` and is only necessary if you wish to use the `tanjun.Client.unload_module` or `tanjun.Client.reload_module`. Parameters ---------- callback : collections.abc.Callable[[tanjun.Client], None]] The callback used to unload Tanjun components from a module. This should take one argument of type `Client` (or `tanjun.abc.Client` if `standard_impl` is `False`), return nothing and will be expected to remove utilities such as components from the provided client. standard_impl : bool Whether this unloader should only allow instances of `Client` as opposed to `tanjun.abc.Client`. Defaults to `True`. Returns ------- collections.abc.Callable[[tanjun.Client], None]] The decorated unload callback. """ return _UnloaderDescriptor(callback, standard_impl)
Mark a callback as being used to unload a module's utilities from a client.
Note:
This is the inverse of as_loader and is only necessary if you wish
to use the tanjun.Client.unload_module or
tanjun.Client.reload_module.
Parameters
callback (collections.abc.Callable[[tanjun.Client], None]]): The callback used to unload Tanjun components from a module.
This should take one argument of type
Client(ortanjun.abc.Clientifstandard_implisFalse), return nothing and will be expected to remove utilities such as components from the provided client.standard_impl (bool): Whether this unloader should only allow instances of
Clientas opposed totanjun.abc.Client.Defaults to
True.
Returns
- collections.abc.Callable[[tanjun.Client], None]]: The decorated unload callback.
View Source
class Client(injecting.InjectorClient, tanjun_abc.Client): """Tanjun's standard `tanjun.abc.Client` implementation. This implementation supports dependency injection for checks, command callbacks, prefix getters and event listeners. For more information on how this works see `tanjun.injecting`. .. note:: By default this client includes a parser error handling hook which will by overwritten if you call `Client.set_hooks`. """ __slots__ = ( "_accepts", "_auto_defer_after", "_cache", "_cached_application_id", "_checks", "_client_callbacks", "_components", "_defaults_to_ephemeral", "_make_message_context", "_make_slash_context", "_events", "_grab_mention_prefix", "_hooks", "_interaction_not_found", "_slash_hooks", "_is_closing", "_listeners", "_loop", "_message_hooks", "_metadata", "_modules", "_path_modules", "_prefix_getter", "_prefixes", "_rest", "_server", "_shards", "_voice", ) def __init__( self, rest: hikari.api.RESTClient, *, cache: typing.Optional[hikari.api.Cache] = None, events: typing.Optional[hikari.api.EventManager] = None, server: typing.Optional[hikari.api.InteractionServer] = None, shards: typing.Optional[hikari_traits.ShardAware] = None, voice: typing.Optional[hikari.api.VoiceComponent] = None, event_managed: bool = False, mention_prefix: bool = False, set_global_commands: typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] = False, declare_global_commands: typing.Union[ hikari.SnowflakeishSequence[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool ] = False, command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None, _stack_level: int = 0, ) -> None: """Initialise a Tanjun client. Notes ----- * For a quicker way to initiate this client around a standard bot aware client, see `Client.from_gateway_bot` and `Client.from_rest_bot`. * The endpoint used by `declare_global_commands` has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally). * `event_manager` is necessary for message command dispatch and will also be necessary for interaction command dispatch if `server` isn't provided. * `server` is used for interaction command dispatch if interaction events aren't being received from the event manager. Parameters ---------- rest : hikari.api.rest.RestClient The Hikari REST client this will use. Other Parameters ---------------- cache : hikari.api.cache.CacheClient The Hikari cache client this will use if applicable. event_manager : hikari.api.event_manager.EventManagerClient The Hikari event manager client this will use if applicable. server : hikari.api.interaction_server.InteractionServer The Hikari interaction server client this will use if applicable. shards : hikari.traits.ShardAware The Hikari shard aware client this will use if applicable. voice : hikari.api.voice.VoiceComponent The Hikari voice component this will use if applicable. event_managed : bool Whether or not this client is managed by the event manager. An event managed client will be automatically started and closed based on Hikari's lifetime events. Defaults to `False` and can only be passed as `True` if `event_manager` is also provided. mention_prefix : bool Whether or not mention prefixes should be automatically set when this client is first started. Defaults to `False` and it should be noted that this only applies to message commands. declare_global_commands : typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool] Whether or not to automatically set global slash commands when this client is first started. Defaults to `False`. If one or more guild objects/IDs are passed here then the registered global commands will be set on the specified guild(s) at startup rather than globally. This can be useful for testing/debug purposes as slash commands may take up to an hour to propagate globally but will immediately propagate when set on a specific guild. set_global_commands : typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] Deprecated as of v2.1.1a1 alias of `declare_global_commands`. command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] If provided, a mapping of top level command names to IDs of the commands to update. This field is complementary to `declare_global_commands` and, while it isn't necessarily required, this will in some situations help avoid permissions which were previously set for a command from being lost after a rename. This currently isn't supported when multiple guild IDs are passed for `declare_global_commands`. Raises ------ ValueError Raises for the following reasons: * If `event_managed` is `True` when `event_manager` is `None`. * If `command_ids` is passed when multiple guild ids are provided for `declare_global_commands`. * If `command_ids` is passed when `declare_global_commands` is `False`. """ # noqa: E501 - line too long # InjectorClient.__init__ super().__init__() if _LOGGER.isEnabledFor(logging.INFO): _LOGGER.info( "%s initialised with the following components: %s", "Event-managed client" if event_managed else "Client", ", ".join( name for name, value in [ ("cache", cache), ("event manager", events), ("interaction server", server), ("rest", rest), ("shard manager", shards), ] if value ), ) if not events and not server: _LOGGER.warning( "Client initiaited without an event manager or interaction server, " "automatic command dispatch will be unavailable." ) self._accepts = MessageAcceptsEnum.ALL if events else MessageAcceptsEnum.NONE self._auto_defer_after: typing.Optional[float] = 2.0 self._cache = cache self._cached_application_id: typing.Optional[hikari.Snowflake] = None self._checks: list[checks.InjectableCheck] = [] self._client_callbacks: dict[str, list[injecting.CallbackDescriptor[None]]] = {} self._components: dict[str, tanjun_abc.Component] = {} self._defaults_to_ephemeral: bool = False self._make_message_context: _MessageContextMakerProto = context.MessageContext self._make_slash_context: _SlashContextMakerProto = context.SlashContext self._events = events self._grab_mention_prefix = mention_prefix self._hooks: typing.Optional[tanjun_abc.AnyHooks] = hooks.AnyHooks().set_on_parser_error(on_parser_error) self._interaction_not_found: typing.Optional[str] = "Command not found" self._slash_hooks: typing.Optional[tanjun_abc.SlashHooks] = None self._is_closing = False self._listeners: dict[type[hikari.Event], list[injecting.SelfInjectingCallback[None]]] = {} self._loop: typing.Optional[asyncio.AbstractEventLoop] = None self._message_hooks: typing.Optional[tanjun_abc.MessageHooks] = None self._metadata: dict[typing.Any, typing.Any] = {} self._modules: dict[str, types.ModuleType] = {} self._path_modules: dict[pathlib.Path, types.ModuleType] = {} self._prefix_getter: typing.Optional[injecting.CallbackDescriptor[collections.Iterable[str]]] = None self._prefixes: list[str] = [] self._rest = rest self._server = server self._shards = shards self._voice = voice if event_managed: if not events: raise ValueError("Client cannot be event managed without an event manager") events.subscribe(hikari.StartingEvent, self._on_starting_event) events.subscribe(hikari.StoppingEvent, self._on_stopping_event) if set_global_commands: warnings.warn( "The `set_global_commands` argument is deprecated since v2.1.1a1. " "Use `declare_global_commands` instead.", DeprecationWarning, stacklevel=2 + _stack_level, ) declare_global_commands = declare_global_commands or set_global_commands command_ids = command_ids or {} if isinstance(declare_global_commands, collections.Sequence): if command_ids and len(declare_global_commands) > 1: raise ValueError( "Cannot provide specific command_ids while automatically " "declaring commands marked as 'global' in multiple-guilds on startup" ) for guild in declare_global_commands: _LOGGER.info("Registering startup command declarer for %s guild", guild) self.add_client_callback(ClientCallbackNames.STARTING, _StartDeclarer(self, command_ids, guild)) elif isinstance(declare_global_commands, bool): if declare_global_commands: _LOGGER.info("Registering startup command declarer for global commands") if not command_ids: _LOGGER.warning( "No command IDs passed for startup command declarer, this could lead to previously set " "command permissions being lost when commands are renamed." ) self.add_client_callback( ClientCallbackNames.STARTING, _StartDeclarer(self, command_ids, hikari.UNDEFINED) ) elif command_ids: raise ValueError("Cannot pass command IDs when not declaring global commands") else: self.add_client_callback( ClientCallbackNames.STARTING, _StartDeclarer(self, command_ids, declare_global_commands) ) ( self.set_type_dependency(tanjun_abc.Client, self) .set_type_dependency(Client, self) .set_type_dependency(type(self), self) .set_type_dependency(hikari.api.RESTClient, rest) .set_type_dependency(type(rest), rest) ) if cache: self.set_type_dependency(hikari.api.Cache, cache).set_type_dependency(type(cache), cache) if events: self.set_type_dependency(hikari.api.EventManager, events).set_type_dependency(type(events), events) if server: self.set_type_dependency(hikari.api.InteractionServer, server).set_type_dependency(type(server), server) if shards: self.set_type_dependency(hikari_traits.ShardAware, shards).set_type_dependency(type(shards), shards) if voice: self.set_type_dependency(hikari.api.VoiceComponent, voice).set_type_dependency(type(voice), voice) dependencies.set_standard_dependencies(self) @classmethod def from_gateway_bot( cls, bot: hikari_traits.GatewayBotAware, /, *, event_managed: bool = True, mention_prefix: bool = False, declare_global_commands: typing.Union[ hikari.SnowflakeishSequence[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool ] = False, set_global_commands: typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] = False, command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None, ) -> Client: """Build a `Client` from a `hikari.traits.GatewayBotAware` instance. Notes ----- * This implicitly defaults the client to human only mode. * This sets type dependency injectors for the hikari traits present in `bot` (including `hikari.traits.GatewayBotAware`). * The endpoint used by `declare_global_commands` has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally). Parameters ---------- bot : hikari.traits.GatewayBotAware The bot client to build from. This will be used to infer the relevant Hikari clients to use. Other Parameters ---------------- event_managed : bool Whether or not this client is managed by the event manager. An event managed client will be automatically started and closed based on Hikari's lifetime events. Defaults to `True`. mention_prefix : bool Whether or not mention prefixes should be automatically set when this client is first started. Defaults to `False` and it should be noted that this only applies to message commands. declare_global_commands : typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool] Whether or not to automatically set global slash commands when this client is first started. Defaults to `False`. If one or more guild objects/IDs are passed here then the registered global commands will be set on the specified guild(s) at startup rather than globally. This can be useful for testing/debug purposes as slash commands may take up to an hour to propagate globally but will immediately propagate when set on a specific guild. set_global_commands : typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] Deprecated as of v2.1.1a1 alias of `declare_global_commands`. command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] If provided, a mapping of top level command names to IDs of the commands to update. This field is complementary to `declare_global_commands` and, while it isn't necessarily required, this will in some situations help avoid permissions which were previously set for a command from being lost after a rename. This currently isn't supported when multiple guild IDs are passed for `declare_global_commands`. """ # noqa: E501 - line too long return ( cls( rest=bot.rest, cache=bot.cache, events=bot.event_manager, shards=bot, voice=bot.voice, event_managed=event_managed, mention_prefix=mention_prefix, declare_global_commands=declare_global_commands, set_global_commands=set_global_commands, command_ids=command_ids, _stack_level=1, ) .set_human_only() .set_hikari_trait_injectors(bot) ) @classmethod def from_rest_bot( cls, bot: hikari_traits.RESTBotAware, /, *, declare_global_commands: typing.Union[ hikari.SnowflakeishSequence[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool ] = False, set_global_commands: typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] = False, command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None, ) -> Client: """Build a `Client` from a `hikari.traits.RESTBotAware` instance. Notes ----- * This sets type dependency injectors for the hikari traits present in `bot` (including `hikari.traits.RESTBotAware`). * The endpoint used by `declare_global_commands` has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally). Parameters ---------- bot : hikari.traits.RESTBotAware The bot client to build from. Other Parameters ---------------- declare_global_commands : typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool] Whether or not to automatically set global slash commands when this client is first started. Defaults to `False`. If one or more guild objects/IDs are passed here then the registered global commands will be set on the specified guild(s) at startup rather than globally. This can be useful for testing/debug purposes as slash commands may take up to an hour to propagate globally but will immediately propagate when set on a specific guild. set_global_commands : typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] Deprecated as of v2.1.1a1 alias of `declare_global_commands`. command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] If provided, a mapping of top level command names to IDs of the commands to update. This field is complementary to `declare_global_commands` and, while it isn't necessarily required, this will in some situations help avoid permissions which were previously set for a command from being lost after a rename. This currently isn't supported when multiple guild IDs are passed for `declare_global_commands`. """ # noqa: E501 - line too long return cls( rest=bot.rest, server=bot.interaction_server, declare_global_commands=declare_global_commands, set_global_commands=set_global_commands, command_ids=command_ids, _stack_level=1, ).set_hikari_trait_injectors(bot) async def __aenter__(self) -> Client: await self.open() return self async def __aexit__( self, exc_type: typing.Optional[type[Exception]], exc: typing.Optional[Exception], exc_traceback: typing.Optional[types.TracebackType], ) -> None: await self.close() def __repr__(self) -> str: return f"CommandClient <{type(self).__name__!r}, {len(self._components)} components, {self._prefixes}>" @property def defaults_to_ephemeral(self) -> bool: # <<inherited docstring from tanjun.abc.Client>>. return self._defaults_to_ephemeral @property def message_accepts(self) -> MessageAcceptsEnum: """Type of message create events this command client accepts for execution.""" return self._accepts @property def is_human_only(self) -> bool: """Whether this client is only executing for non-bot/webhook users messages.""" return typing.cast("checks.InjectableCheck", _check_human) in self._checks @property def cache(self) -> typing.Optional[hikari.api.Cache]: # <<inherited docstring from tanjun.abc.Client>>. return self._cache @property def checks(self) -> collections.Collection[tanjun_abc.CheckSig]: """Collection of the level `tanjun.abc.Context` checks registered to this client. .. note:: These may be taking advantage of the standard dependency injection. """ return tuple(check.callback for check in self._checks) @property def components(self) -> collections.Collection[tanjun_abc.Component]: # <<inherited docstring from tanjun.abc.Client>>. return self._components.copy().values() @property def events(self) -> typing.Optional[hikari.api.EventManager]: # <<inherited docstring from tanjun.abc.Client>>. return self._events @property def listeners( self, ) -> collections.Mapping[type[hikari.Event], collections.Collection[tanjun_abc.ListenerCallbackSig]]: return utilities.CastedView( self._listeners, lambda x: [typing.cast(tanjun_abc.ListenerCallbackSig, callback.callback) for callback in x], ) @property def hooks(self) -> typing.Optional[tanjun_abc.AnyHooks]: """Top level `tanjun.abc.AnyHooks` set for this client. These are called during both message and interaction command execution. Returns ------- typing.Optional[tanjun.abc.AnyHooks] The top level `tanjun.abc.Context` based hooks set for this client if applicable, else `None`. """ return self._hooks @property def slash_hooks(self) -> typing.Optional[tanjun_abc.SlashHooks]: """Top level `tanjun.abc.SlashHooks` set for this client. These are only called during interaction command execution. """ return self._slash_hooks @property def is_alive(self) -> bool: # <<inherited docstring from tanjun.abc.Client>>. return self._loop is not None @property def loop(self) -> typing.Optional[asyncio.AbstractEventLoop]: # <<inherited docstring from tanjun.abc.Client>>. return self._loop @property def message_hooks(self) -> typing.Optional[tanjun_abc.MessageHooks]: """Top level `tanjun.abc.MessageHooks` set for this client. These are only called during both message command execution. """ return self._message_hooks @property def metadata(self) -> collections.MutableMapping[typing.Any, typing.Any]: # <<inherited docstring from tanjun.abc.Client>>. return self._metadata @property def prefix_getter(self) -> typing.Optional[PrefixGetterSig]: """Prefix getter method set for this client. For more information on this callback's signature see `PrefixGetter`. """ return typing.cast(PrefixGetterSig, self._prefix_getter.callback) if self._prefix_getter else None @property def prefixes(self) -> collections.Collection[str]: """Collection of the standard prefixes set for this client.""" return self._prefixes.copy() @property def rest(self) -> hikari.api.RESTClient: # <<inherited docstring from tanjun.abc.Client>>. return self._rest @property def server(self) -> typing.Optional[hikari.api.InteractionServer]: # <<inherited docstring from tanjun.abc.Client>>. return self._server @property def shards(self) -> typing.Optional[hikari_traits.ShardAware]: # <<inherited docstring from tanjun.abc.Client>>. return self._shards @property def voice(self) -> typing.Optional[hikari.api.VoiceComponent]: # <<inherited docstring from tanjun.abc.Client>>. return self._voice async def _on_starting_event(self, _: hikari.StartingEvent, /) -> None: await self.open() async def _on_stopping_event(self, _: hikari.StoppingEvent, /) -> None: await self.close() async def clear_application_commands( self, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, ) -> None: # <<inherited docstring from tanjun.abc.Client>>. if application is None: application = self._cached_application_id or await self.fetch_rest_application_id() await self._rest.set_application_commands(application, (), guild=guild) async def set_global_commands( self, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, force: bool = False, ) -> collections.Sequence[hikari.Command]: """Alias of `Client.declare_global_commands`. .. deprecated:: v2.1.1a1 Use `Client.declare_global_commands` instead. """ warnings.warn( "The `Client.set_global_commands` method has been deprecated since v2.1.1a1. " "Use `Client.declare_global_commands` instead.", DeprecationWarning, stacklevel=2, ) return await self.declare_global_commands(application=application, guild=guild, force=force) async def declare_global_commands( self, command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, force: bool = False, ) -> collections.Sequence[hikari.Command]: # <<inherited docstring from tanjun.abc.Client>>. commands = ( command for command in itertools.chain.from_iterable( component.slash_commands for component in self._components.values() ) if command.is_global ) return await self.declare_application_commands( commands, command_ids, application=application, guild=guild, force=force ) async def declare_application_command( self, command: tanjun_abc.BaseSlashCommand, /, command_id: typing.Optional[hikari.Snowflakeish] = None, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, ) -> hikari.Command: # <<inherited docstring from tanjun.abc.Client>>. builder = command.build() if command_id: response = await self._rest.edit_application_command( application or self._cached_application_id or await self.fetch_rest_application_id(), command_id, guild=guild, name=builder.name, description=builder.description, options=builder.options, ) else: response = await self._rest.create_application_command( application or self._cached_application_id or await self.fetch_rest_application_id(), guild=guild, name=builder.name, description=builder.description, options=builder.options, ) if not guild: command.set_tracked_command(response) # TODO: is this fine? return response async def declare_application_commands( self, commands: collections.Iterable[tanjun_abc.BaseSlashCommand], /, command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, force: bool = False, ) -> collections.Sequence[hikari.Command]: # <<inherited docstring from tanjun.abc.Client>>. command_ids = command_ids or {} names_to_commands: dict[str, tanjun_abc.BaseSlashCommand] = {} conflicts: set[str] = set() builders: dict[str, hikari.api.CommandBuilder] = {} for command in commands: names_to_commands[command.name] = command if command.name in builders: conflicts.add(command.name) builder = command.build() if command_id := command_ids.get(command.name): builder.set_id(hikari.Snowflake(command_id)) builders[command.name] = builder if conflicts: raise ValueError( "Couldn't declare commands due to conflicts. The following command names have more than one command " "registered for them " + ", ".join(conflicts) ) if len(builders) > 100: raise ValueError("You can only declare up to 100 top level commands in a guild or globally") if not application: application = self._cached_application_id or await self.fetch_rest_application_id() target_type = "global" if guild is hikari.UNDEFINED else f"guild {int(guild)}" if not force: registered_commands = await self._rest.fetch_application_commands(application, guild=guild) if len(registered_commands) == len(builders) and all( _cmp_command(builders.get(command.name), command) for command in registered_commands ): _LOGGER.info("Skipping bulk declare for %s slash commands since they're already declared", target_type) return registered_commands _LOGGER.info("Bulk declaring %s %s slash commands", len(builders), target_type) responses = await self._rest.set_application_commands(application, list(builders.values()), guild=guild) for response in responses: if not guild: names_to_commands[response.name].set_tracked_command(response) # TODO: is this fine? if (expected_id := command_ids.get(response.name)) and hikari.Snowflake(expected_id) != response.id: _LOGGER.warning( "ID mismatch found for %s command %r, expected %s but got %s. " "This suggests that any previous permissions set for this command will have been lost.", target_type, response.name, expected_id, response.id, ) _LOGGER.info("Successfully declared %s (top-level) %s commands", len(responses), target_type) if _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug( "Declared %s command ids; %s", target_type, ", ".join(f"{response.name}: {response.id}" for response in responses), ) return responses def set_auto_defer_after(self: _ClientT, time: typing.Optional[float], /) -> _ClientT: """Set when this client should automatically defer execution of commands. .. warning:: If `time` is set to `None` then automatic deferrals will be disabled. This may lead to unexpected behaviour. Parameters ---------- time : typing.Optional[float] The time in seconds to defer interaction command responses after. """ self._auto_defer_after = float(time) if time is not None else None return self def set_ephemeral_default(self: _ClientT, state: bool, /) -> _ClientT: """Set whether slash contexts spawned by this client should default to ephemeral responses. Parameters ---------- bool Whether slash command contexts executed in this component should should default to ephemeral. This will be overridden by any response calls which specify flags and defaults to `False`. Returns ------- SelfT This component to enable method chaining. """ self._defaults_to_ephemeral = state return self def set_hikari_trait_injectors(self: _ClientT, bot: hikari_traits.RESTAware, /) -> _ClientT: """Set type based dependency injection based on the hikari traits found in `bot`. This is a short hand for calling `Client.add_type_dependency` for all the hikari trait types `bot` is valid for with bot. Parameters ---------- bot : hikari_traits.RESTAware The hikari client to set dependency injectors for. """ for _, member in inspect.getmembers(hikari_traits): if inspect.isclass(member) and isinstance(bot, member): self.set_type_dependency(member, bot) return self def set_interaction_not_found(self: _ClientT, message: typing.Optional[str], /) -> _ClientT: """Set the response message for when an interaction command is not found. .. warning:: Setting this to `None` may lead to unexpected behaviour (especially when the client is still set to auto-defer interactions) and should only be done if you know what you're doing. Parameters ---------- message : typing.Optional[str] The message to respond with when an interaction command isn't found. """ self._interaction_not_found = message return self def set_message_accepts(self: _ClientT, accepts: MessageAcceptsEnum, /) -> _ClientT: """Set the kind of messages commands should be executed based on. Parameters ---------- accepts : MessageAcceptsEnum The type of messages commands should be executed based on. """ if accepts.get_event_type() and not self._events: raise ValueError("Cannot set accepts level on a client with no event manager") self._accepts = accepts return self def set_message_ctx_maker(self: _ClientT, maker: _MessageContextMakerProto = context.MessageContext, /) -> _ClientT: """Set the message context maker to use when creating context for a message. .. warning:: The caller must return an instance of `tanjun.context.MessageContext` rather than just any implementation of the MessageContext abc due to this client relying on implementation detail of `tanjun.context.MessageContext`. Parameters ---------- maker : _MessageContextMakerProto The message context maker to use. This is a callback which should match the signature of `tanjun.context.MessageContext.__init__` and return an instance of `tanjun.context.MessageContext`. This defaults to `tanjun.context.MessageContext`. """ self._make_message_context = maker return self def set_metadata(self: _ClientT, key: typing.Any, value: typing.Any, /) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. self._metadata[key] = value return self def set_slash_ctx_maker(self: _ClientT, maker: _SlashContextMakerProto = context.SlashContext, /) -> _ClientT: """Set the slash context maker to use when creating context for a slash command. .. warning:: The caller must return an instance of `tanjun.context.SlashContext` rather than just any implementation of the SlashContext abc due to this client relying on implementation detail of `tanjun.context.SlashContext`. Parameters ---------- maker : _SlashContextMakerProto The slash context maker to use. This is a callback which should match the signature of `tanjun.context.SlashContext.__init__` and return an instance of `tanjun.context.SlashContext`. This defaults to `tanjun.context.SlashContext`. """ self._make_slash_context = maker return self def set_human_only(self: _ClientT, value: bool = True) -> _ClientT: """Set whether or not message commands execution should be limited to "human" users. .. note:: This doesn't apply to interaction commands as these can only be triggered by a "human" (normal user account). Parameters ---------- value : bool Whether or not message commands execution should be limited to "human" users. Passing `True` here will prevent message commands from being executed based on webhook and bot messages. """ if value: self.add_check(_check_human) else: try: self.remove_check(_check_human) except ValueError: pass return self def add_check(self: _ClientT, check: tanjun_abc.CheckSig, /) -> _ClientT: """Add a generic check to this client. This will be applied to both message and slash command execution. Parameters ---------- check : tanjun_abc.CheckSig The check to add. This may be either synchronous or asynchronous and must take one positional argument of type `tanjun.abc.Context` with dependency injection being supported for its keyword arguments. Returns ------- Self The client instance to enable chained calls. """ if check not in self._checks: self._checks.append(checks.InjectableCheck(check)) return self def remove_check(self: _ClientT, check: tanjun_abc.CheckSig, /) -> _ClientT: """Remove a check from the client. Parameters ---------- check : tanjun_abc.CheckSig The check to remove. Raises ------ ValueError If the check was not previously added. """ self._checks.remove(typing.cast("checks.InjectableCheck", check)) return self def with_check(self, check: tanjun_abc.CheckSigT, /) -> tanjun_abc.CheckSigT: """Add a check to this client through a decorator call. Parameters ---------- check : tanjun_abc.CheckSig The check to add. This may be either synchronous or asynchronous and must take one positional argument of type `tanjun.abc.Context` with dependency injection being supported for its keyword arguments. Returns ------- tanjun_abc.CheckSig The added check. """ self.add_check(check) return check async def check(self, ctx: tanjun_abc.Context, /) -> bool: return await utilities.gather_checks(ctx, self._checks) def add_component(self: _ClientT, component: tanjun_abc.Component, /, *, add_injector: bool = False) -> _ClientT: """Add a component to this client. Parameters ---------- component: Component The component to move to this client. Returns ------- Self The client instance to allow chained calls. Raises ------ ValueError If the component's name is already registered. """ if component.name in self._components: raise ValueError(f"A component named {component.name!r} is already registered.") component.bind_client(self) self._components[component.name] = component if add_injector: self.set_type_dependency(type(component), lambda: component) if self._loop: self._loop.create_task(component.open()) self._loop.create_task(self.dispatch_client_callback(ClientCallbackNames.COMPONENT_ADDED, component)) return self def get_component_by_name(self, name: str, /) -> typing.Optional[tanjun_abc.Component]: # <<inherited docstring from tanjun.abc.Client>>. return self._components.get(name) def remove_component(self: _ClientT, component: tanjun_abc.Component, /) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. stored_component = self._components.get(component.name) if not stored_component or stored_component != component: raise ValueError(f"The component {component!r} is not registered.") del self._components[component.name] if self._loop: self._loop.create_task(component.close(unbind=True)) self._loop.create_task( self.dispatch_client_callback(ClientCallbackNames.COMPONENT_REMOVED, stored_component) ) else: stored_component.unbind_client(self) return self def remove_component_by_name(self: _ClientT, name: str, /) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. return self.remove_component(self._components[name]) def add_client_callback( self: _ClientT, name: typing.Union[str, tanjun_abc.ClientCallbackNames], callback: tanjun_abc.MetaEventSig, / ) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. descriptor = injecting.CallbackDescriptor(callback) name = name.casefold() try: if descriptor in self._client_callbacks[name]: return self self._client_callbacks[name].append(descriptor) except KeyError: self._client_callbacks[name] = [descriptor] return self async def dispatch_client_callback( self, name: typing.Union[str, tanjun_abc.ClientCallbackNames], /, *args: typing.Any ) -> None: # <<inherited docstring from tanjun.abc.Client>>. name = name.casefold() if callbacks := self._client_callbacks.get(name): calls = ( _wrap_client_callback(callback, injecting.BasicInjectionContext(self), args) for callback in callbacks ) await asyncio.gather(*calls) def get_client_callbacks( self, name: typing.Union[str, tanjun_abc.ClientCallbackNames], / ) -> collections.Collection[tanjun_abc.MetaEventSig]: # <<inherited docstring from tanjun.abc.Client>>. name = name.casefold() if result := self._client_callbacks.get(name): return tuple(callback.callback for callback in result) return () def remove_client_callback( self: _ClientT, name: typing.Union[str, tanjun_abc.ClientCallbackNames], callback: tanjun_abc.MetaEventSig, / ) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. name = name.casefold() self._client_callbacks[name].remove(typing.cast("injecting.CallbackDescriptor[None]", callback)) if not self._client_callbacks[name]: del self._client_callbacks[name] return self def with_client_callback( self, name: typing.Union[str, tanjun_abc.ClientCallbackNames], / ) -> collections.Callable[[tanjun_abc.MetaEventSigT], tanjun_abc.MetaEventSigT]: # <<inherited docstring from tanjun.abc.Client>>. def decorator(callback: tanjun_abc.MetaEventSigT, /) -> tanjun_abc.MetaEventSigT: self.add_client_callback(name, callback) return callback return decorator def add_listener( self: _ClientT, event_type: type[hikari.Event], callback: tanjun_abc.ListenerCallbackSig, / ) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. injected: injecting.SelfInjectingCallback[None] = injecting.SelfInjectingCallback(self, callback) try: if callback in self._listeners[event_type]: return self self._listeners[event_type].append(injected) except KeyError: self._listeners[event_type] = [injected] if self._loop and self._events: self._events.subscribe(event_type, injected.__call__) return self def remove_listener( self: _ClientT, event_type: type[hikari.Event], callback: tanjun_abc.ListenerCallbackSig, / ) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. index = self._listeners[event_type].index(typing.cast("injecting.SelfInjectingCallback[None]", callback)) registered_callback = self._listeners[event_type].pop(index) if not self._listeners[event_type]: del self._listeners[event_type] if self._loop and self._events: self._events.unsubscribe(event_type, registered_callback.__call__) return self def with_listener( self, event_type: type[hikari.Event], / ) -> collections.Callable[[tanjun_abc.ListenerCallbackSigT], tanjun_abc.ListenerCallbackSigT]: # <<inherited docstring from tanjun.abc.Client>>. def decorator(callback: tanjun_abc.ListenerCallbackSigT, /) -> tanjun_abc.ListenerCallbackSigT: self.add_listener(event_type, callback) return callback return decorator def add_prefix(self: _ClientT, prefixes: typing.Union[collections.Iterable[str], str], /) -> _ClientT: """Add a prefix used to filter message command calls. This will be matched against the first character(s) in a message's content to determine whether the message command search stage of execution should be initiated. Parameters ---------- prefixes : typing.Union[collections.abc.Iterable[str], str] Either a single string or an iterable of strings to be used as prefixes. Returns ------- Self The client instance to enable chained calls. """ if isinstance(prefixes, str): if prefixes not in self._prefixes: self._prefixes.append(prefixes) else: self._prefixes.extend(prefix for prefix in prefixes if prefix not in self._prefixes) return self def remove_prefix(self: _ClientT, prefix: str, /) -> _ClientT: """Remove a message content prefix from the client. Parameters ---------- prefix : str The prefix to remove. Raises ------ ValueError If the prefix is not registered with the client. Returns ------- Self The client instance to enable chained calls. """ self._prefixes.remove(prefix) return self def set_prefix_getter(self: _ClientT, getter: typing.Optional[PrefixGetterSig], /) -> _ClientT: """Set the callback used to retrieve message prefixes set for the relevant guild. Parameters ---------- getter : typing.Optional[PrefixGetterSig] The callback which'll be used to retrieve prefixes for the guild a message context is from. If `None` is passed here then the callback will be unset. This should be an async callback which one argument of type `tanjun.abc.MessageContext` and returns an iterable of string prefixes. Dependency injection is supported for this callback's keyword arguments. Returns ------- Self The client instance to enable chained calls. """ self._prefix_getter = injecting.CallbackDescriptor(getter) if getter else None return self def with_prefix_getter(self, getter: PrefixGetterSigT, /) -> PrefixGetterSigT: """Set the prefix getter callback for this client through decorator call. Examples -------- ```py client = tanjun.Client.from_rest_bot(bot) @client.with_prefix_getter async def prefix_getter(ctx: tanjun.abc.MessageContext) -> collections.abc.Iterable[str]: raise NotImplementedError ``` Parameters ---------- getter : PrefixGetterSig The callback which'll be to retrieve prefixes for the guild a message event is from. This should be an async callback which one argument of type `tanjun.abc.MessageContext` and returns an iterable of string prefixes. Dependency injection is supported for this callback's keyword arguments. Returns ------- PrefixGetterSigT The registered callback. """ self.set_prefix_getter(getter) return getter def iter_commands(self) -> collections.Iterator[tanjun_abc.ExecutableCommand[tanjun_abc.Context]]: # <<inherited docstring from tanjun.abc.Client>>. slash_commands = self.iter_slash_commands(global_only=False) yield from self.iter_message_commands() yield from slash_commands def iter_message_commands(self) -> collections.Iterator[tanjun_abc.MessageCommand[typing.Any]]: # <<inherited docstring from tanjun.abc.Client>>. return itertools.chain.from_iterable(component.message_commands for component in self.components) def iter_slash_commands(self, *, global_only: bool = False) -> collections.Iterator[tanjun_abc.BaseSlashCommand]: # <<inherited docstring from tanjun.abc.Client>>. if global_only: return filter(lambda c: c.is_global, self.iter_slash_commands(global_only=False)) return itertools.chain.from_iterable(component.slash_commands for component in self.components) def check_message_name( self, name: str, / ) -> collections.Iterator[tuple[str, tanjun_abc.MessageCommand[typing.Any]]]: # <<inherited docstring from tanjun.abc.Client>>. return itertools.chain.from_iterable( component.check_message_name(name) for component in self._components.values() ) def check_slash_name(self, name: str, /) -> collections.Iterator[tanjun_abc.BaseSlashCommand]: # <<inherited docstring from tanjun.abc.Client>>. return itertools.chain.from_iterable( component.check_slash_name(name) for component in self._components.values() ) async def _check_prefix(self, ctx: tanjun_abc.MessageContext, /) -> typing.Optional[str]: if self._prefix_getter: for prefix in await self._prefix_getter.resolve_with_command_context(ctx, ctx): if ctx.content.startswith(prefix): return prefix for prefix in self._prefixes: if ctx.content.startswith(prefix): return prefix return None def _try_unsubscribe( self, event_manager: hikari.api.EventManager, event_type: type[hikari.Event], callback: tanjun_abc.ListenerCallbackSig, ) -> None: try: event_manager.unsubscribe(event_type, callback) except (ValueError, LookupError): # TODO: add logging here pass async def close(self, *, deregister_listeners: bool = True) -> None: """Close the client. Raises ------ RuntimeError If the client isn't running. """ if not self._loop: raise RuntimeError("Client isn't active") if self._is_closing: event = asyncio.Event() self.add_client_callback(ClientCallbackNames.CLOSED, event.set) try: await event.wait() finally: self.remove_client_callback(ClientCallbackNames.CLOSED, event.set) return self._is_closing = True await self.dispatch_client_callback(ClientCallbackNames.CLOSING) if deregister_listeners and self._events: if event_type := self._accepts.get_event_type(): self._try_unsubscribe(self._events, event_type, self.on_message_create_event) self._try_unsubscribe(self._events, hikari.InteractionCreateEvent, self.on_interaction_create_event) for event_type_, listeners in self._listeners.items(): for listener in listeners: self._try_unsubscribe(self._events, event_type_, listener.__call__) if deregister_listeners and self._server: self._server.set_listener(hikari.CommandInteraction, None) await asyncio.gather(*(component.close() for component in self._components.copy().values())) self._loop = None await self.dispatch_client_callback(ClientCallbackNames.CLOSED) self._is_closing = False async def open(self, *, register_listeners: bool = True) -> None: """Start the client. If `mention_prefix` was passed to `Client.__init__` or `Client.from_gateway_bot` then this function may make a fetch request to Discord if it cannot get the current user from the cache. Raises ------ RuntimeError If the client is already active. """ if self._loop: raise RuntimeError("Client is already alive") self._loop = asyncio.get_running_loop() self._is_closing = False await self.dispatch_client_callback(ClientCallbackNames.STARTING) if self._grab_mention_prefix: user: typing.Optional[hikari.OwnUser] = None if self._cache: user = self._cache.get_me() if not user and (user_cache := self.get_type_dependency(dependencies.SingleStoreCache[hikari.OwnUser])): user = await user_cache.get(default=None) if not user: user = await self._rest.fetch_my_user() for prefix in f"<@{user.id}>", f"<@!{user.id}>": if prefix not in self._prefixes: self._prefixes.append(prefix) self._grab_mention_prefix = False await asyncio.gather(*(component.open() for component in self._components.copy().values())) if register_listeners and self._events: if event_type := self._accepts.get_event_type(): self._events.subscribe(event_type, self.on_message_create_event) self._events.subscribe(hikari.InteractionCreateEvent, self.on_interaction_create_event) for event_type_, listeners in self._listeners.items(): for listener in listeners: self._events.subscribe(event_type_, listener.__call__) if register_listeners and self._server: self._server.set_listener(hikari.CommandInteraction, self.on_interaction_create_request) self._loop.create_task(self.dispatch_client_callback(ClientCallbackNames.STARTED)) async def fetch_rest_application_id(self) -> hikari.Snowflake: """Fetch the ID of the application this client is linked to. Returns ------- hikari.Snowflake The application ID of the application this client is linked to. """ if self._cached_application_id: return self._cached_application_id application_cache = self.get_type_dependency( dependencies.SingleStoreCache[hikari.Application] ) or self.get_type_dependency(dependencies.SingleStoreCache[hikari.AuthorizationApplication]) if application_cache and (application := await application_cache.get(default=None)): self._cached_application_id = application.id return application.id if self._rest.token_type == hikari.TokenType.BOT: self._cached_application_id = hikari.Snowflake(await self._rest.fetch_application()) else: self._cached_application_id = hikari.Snowflake((await self._rest.fetch_authorization()).application) return self._cached_application_id def set_hooks(self: _ClientT, hooks: typing.Optional[tanjun_abc.AnyHooks], /) -> _ClientT: """Set the general command execution hooks for this client. The callbacks within this hook will be added to every slash and message command execution started by this client. Parameters ---------- hooks : typing.Optional[tanjun_abc.AnyHooks] The general command execution hooks to set for this client. Passing `None` will remove all hooks. Returns ------- Self The client instance to enable chained calls. """ self._hooks = hooks return self def set_slash_hooks(self: _ClientT, hooks: typing.Optional[tanjun_abc.SlashHooks], /) -> _ClientT: """Set the slash command execution hooks for this client. The callbacks within this hook will be added to every slash command execution started by this client. Parameters ---------- hooks : typing.Optional[tanjun_abc.SlashHooks] The slash context specific command execution hooks to set for this client. Passing `None` will remove the hooks. Returns ------- Self The client instance to enable chained calls. """ self._slash_hooks = hooks return self def set_message_hooks(self: _ClientT, hooks: typing.Optional[tanjun_abc.MessageHooks], /) -> _ClientT: """Set the message command execution hooks for this client. The callbacks within this hook will be added to every message command execution started by this client. Parameters ---------- hooks : typing.Optional[tanjun_abc.MessageHooks] The message context specific command execution hooks to set for this client. Passing `None` will remove all hooks. Returns ------- Self The client instance to enable chained calls. """ self._message_hooks = hooks return self def _call_loaders( self, module_path: typing.Union[str, pathlib.Path], loaders: list[tanjun_abc.ClientLoader], / ) -> None: found = False for loader in loaders: if loader.load(self): found = True if not found: raise errors.ModuleMissingLoaders(f"Didn't find any loaders in {module_path}", module_path) def _call_unloaders( self, module_path: typing.Union[str, pathlib.Path], loaders: list[tanjun_abc.ClientLoader], / ) -> None: found = False for loader in loaders: if loader.unload(self): found = True if not found: raise errors.ModuleMissingLoaders(f"Didn't find any unloaders in {module_path}", module_path) def _load_module( self, module_path: typing.Union[str, pathlib.Path] ) -> collections.Generator[collections.Callable[[], types.ModuleType], types.ModuleType, None]: if isinstance(module_path, str): if module_path in self._modules: raise errors.ModuleStateConflict(f"module {module_path} already loaded", module_path) _LOGGER.info("Loading from %s", module_path) module = yield lambda: importlib.import_module(module_path) with _WrapLoadError(errors.FailedModuleLoad): self._call_loaders(module_path, _get_loaders(module, module_path)) self._modules[module_path] = module else: module_path_abs = module_path.absolute() if module_path_abs in self._path_modules: raise errors.ModuleStateConflict(f"Module at {module_path} already loaded", module_path) _LOGGER.info("Loading from %s", module_path) module = yield lambda: _get_path_module(module_path) with _WrapLoadError(errors.FailedModuleLoad): self._call_loaders(module_path, _get_loaders(module, module_path)) self._path_modules[module_path_abs] = module def load_modules(self: _ClientT, *modules: typing.Union[str, pathlib.Path]) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. for module_path in modules: if isinstance(module_path, pathlib.Path): module_path = module_path.absolute() generator = self._load_module(module_path) load_module = next(generator) with _WrapLoadError(errors.FailedModuleLoad): module = load_module() try: generator.send(module) except StopIteration: pass else: raise RuntimeError("Generator didn't finish") return self async def load_modules_async(self, *modules: typing.Union[str, pathlib.Path]) -> None: # <<inherited docstring from tanjun.abc.Client>>. loop = asyncio.get_running_loop() for module_path in modules: if isinstance(module_path, pathlib.Path): module_path = await loop.run_in_executor(None, module_path.absolute) generator = self._load_module(module_path) load_module = next(generator) with _WrapLoadError(errors.FailedModuleLoad): module = await loop.run_in_executor(None, load_module) try: generator.send(module) except StopIteration: pass else: raise RuntimeError("Generator didn't finish") def unload_modules(self: _ClientT, *modules: typing.Union[str, pathlib.Path]) -> _ClientT: # <<inherited docstring from tanjun.ab.Client>>. for module_path in modules: if isinstance(module_path, str): modules_dict: dict[typing.Any, types.ModuleType] = self._modules else: modules_dict = self._path_modules module_path = module_path.absolute() module = modules_dict.get(module_path) if not module: raise errors.ModuleStateConflict(f"Module {module_path!s} not loaded", module_path) _LOGGER.info("Unloading from %s", module_path) with _WrapLoadError(errors.FailedModuleUnload): self._call_unloaders(module_path, _get_loaders(module, module_path)) del modules_dict[module_path] return self def _reload_module( self, module_path: typing.Union[str, pathlib.Path] ) -> collections.Generator[collections.Callable[[], types.ModuleType], types.ModuleType, None]: if isinstance(module_path, str): old_module = self._modules.get(module_path) def load_module() -> types.ModuleType: assert old_module return importlib.reload(old_module) modules_dict: dict[typing.Any, types.ModuleType] = self._modules else: old_module = self._path_modules.get(module_path) def load_module() -> types.ModuleType: assert isinstance(module_path, pathlib.Path) return _get_path_module(module_path) modules_dict = self._path_modules if not old_module: raise errors.ModuleStateConflict(f"Module {module_path} not loaded", module_path) _LOGGER.info("Reloading %s", module_path) old_loaders = _get_loaders(old_module, module_path) # We assert that the old module has unloaders early to avoid unnecessarily # importing the new module. if not any(loader.has_unload for loader in old_loaders): raise errors.ModuleMissingLoaders(f"Didn't find any unloaders in old {module_path}", module_path) module = yield load_module loaders = _get_loaders(module, module_path) # We assert that the new module has loaders early to avoid unnecessarily # unloading then rolling back when we know it's going to fail to load. if not any(loader.has_load for loader in loaders): raise errors.ModuleMissingLoaders(f"Didn't find any loaders in new {module_path}", module_path) with _WrapLoadError(errors.FailedModuleUnload): # This will never raise MissingLoaders as we assert this earlier self._call_unloaders(module_path, old_loaders) try: # This will never raise MissingLoaders as we assert this earlier self._call_loaders(module_path, loaders) except Exception as exc: self._call_loaders(module_path, old_loaders) raise errors.FailedModuleLoad from exc else: modules_dict[module_path] = module def reload_modules(self: _ClientT, *modules: typing.Union[str, pathlib.Path]) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. for module_path in modules: if isinstance(module_path, pathlib.Path): module_path = module_path.absolute() generator = self._reload_module(module_path) load_module = next(generator) with _WrapLoadError(errors.FailedModuleLoad): module = load_module() try: generator.send(module) except StopIteration: pass else: raise RuntimeError("Generator didn't finish") return self async def reload_modules_async(self, *modules: typing.Union[str, pathlib.Path]) -> None: # <<inherited docstring from tanjun.abc.Client>>. loop = asyncio.get_running_loop() for module_path in modules: if isinstance(module_path, pathlib.Path): module_path = await loop.run_in_executor(None, module_path.absolute) generator = self._reload_module(module_path) load_module = next(generator) with _WrapLoadError(errors.FailedModuleLoad): module = await loop.run_in_executor(None, load_module) try: generator.send(module) except StopIteration: pass else: raise RuntimeError("Generator didn't finish") async def on_message_create_event(self, event: hikari.MessageCreateEvent, /) -> None: """Execute a message command based on a gateway event. Parameters ---------- hikari.events.message_events.MessageCreateEvent The event to handle. """ if event.message.content is None: return ctx = self._make_message_context( client=self, injection_client=self, content=event.message.content, message=event.message ) if (prefix := await self._check_prefix(ctx)) is None: return ctx.set_content(ctx.content.lstrip()[len(prefix) :].lstrip()).set_triggering_prefix(prefix) hooks: typing.Optional[set[tanjun_abc.MessageHooks]] = None if self._hooks and self._message_hooks: hooks = {self._hooks, self._message_hooks} elif self._hooks: hooks = {self._hooks} elif self._message_hooks: hooks = {self._message_hooks} try: if await self.check(ctx): for component in self._components.values(): if await component.execute_message(ctx, hooks=hooks): return except errors.HaltExecution: pass except errors.CommandError as exc: await ctx.respond(exc.message) return await self.dispatch_client_callback(ClientCallbackNames.MESSAGE_COMMAND_NOT_FOUND, ctx) def _get_slash_hooks(self) -> typing.Optional[set[tanjun_abc.SlashHooks]]: hooks: typing.Optional[set[tanjun_abc.SlashHooks]] = None if self._hooks and self._slash_hooks: hooks = {self._hooks, self._slash_hooks} elif self._hooks: hooks = {self._hooks} elif self._slash_hooks: hooks = {self._slash_hooks} return hooks async def _on_slash_not_found(self, ctx: context.SlashContext) -> None: await self.dispatch_client_callback(ClientCallbackNames.SLASH_COMMAND_NOT_FOUND, ctx) if self._interaction_not_found and not ctx.has_responded: await ctx.create_initial_response(self._interaction_not_found) async def on_interaction_create_event(self, event: hikari.InteractionCreateEvent, /) -> None: """Execute a slash command based on Gateway events. .. note:: Any event where `event.interaction` is not `hikari.CommandInteraction` will be ignored. Parameters ---------- event : hikari.events.interaction_events.InteractionCreateEvent The event to execute commands based on. """ if not isinstance(event.interaction, hikari.CommandInteraction): return ctx = self._make_slash_context( client=self, injection_client=self, interaction=event.interaction, on_not_found=self._on_slash_not_found, default_to_ephemeral=self._defaults_to_ephemeral, ) hooks = self._get_slash_hooks() if self._auto_defer_after is not None: ctx.start_defer_timer(self._auto_defer_after) try: if await self.check(ctx): for component in self._components.values(): # This is set on each iteration to ensure that any component # state which was set to this isn't propagated to other components. ctx.set_ephemeral_default(self._defaults_to_ephemeral) if future := await component.execute_interaction(ctx, hooks=hooks): await future return except errors.HaltExecution: pass except errors.CommandError as exc: await ctx.respond(exc.message) return await ctx.mark_not_found() async def on_interaction_create_request(self, interaction: hikari.CommandInteraction, /) -> context.ResponseTypeT: """Execute a slash command based on received REST requests. Parameters ---------- interaction : hikari.CommandInteraction The interaction to execute a command based on. Returns ------- tanjun.context.ResponseType The initial response to send back to Discord. """ ctx = self._make_slash_context( client=self, injection_client=self, interaction=interaction, on_not_found=self._on_slash_not_found, default_to_ephemeral=self._defaults_to_ephemeral, ) if self._auto_defer_after is not None: ctx.start_defer_timer(self._auto_defer_after) hooks = self._get_slash_hooks() future = ctx.get_response_future() try: if await self.check(ctx): for component in self._components.values(): # This is set on each iteration to ensure that any component # state which was set to this isn't propagated to other components. ctx.set_ephemeral_default(self._defaults_to_ephemeral) if await component.execute_interaction(ctx, hooks=hooks): return await future except errors.HaltExecution: pass except errors.CommandError as exc: # Under very specific timing there may be another future which could set a result while we await # ctx.respond therefore we create a task to avoid any erroneous behaviour from this trying to create # another response before it's returned the initial response. asyncio.get_running_loop().create_task( ctx.respond(exc.message), name=f"{interaction.id} command error responder" ) return await future asyncio.get_running_loop().create_task(ctx.mark_not_found(), name=f"{interaction.id} not found") return await future
Tanjun's standard tanjun.abc.Client implementation.
This implementation supports dependency injection for checks, command
callbacks, prefix getters and event listeners. For more information on how
this works see tanjun.injecting.
Note:
By default this client includes a parser error handling hook which will
by overwritten if you call Client.set_hooks.
View Source
def __init__( self, rest: hikari.api.RESTClient, *, cache: typing.Optional[hikari.api.Cache] = None, events: typing.Optional[hikari.api.EventManager] = None, server: typing.Optional[hikari.api.InteractionServer] = None, shards: typing.Optional[hikari_traits.ShardAware] = None, voice: typing.Optional[hikari.api.VoiceComponent] = None, event_managed: bool = False, mention_prefix: bool = False, set_global_commands: typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] = False, declare_global_commands: typing.Union[ hikari.SnowflakeishSequence[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool ] = False, command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None, _stack_level: int = 0, ) -> None: """Initialise a Tanjun client. Notes ----- * For a quicker way to initiate this client around a standard bot aware client, see `Client.from_gateway_bot` and `Client.from_rest_bot`. * The endpoint used by `declare_global_commands` has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally). * `event_manager` is necessary for message command dispatch and will also be necessary for interaction command dispatch if `server` isn't provided. * `server` is used for interaction command dispatch if interaction events aren't being received from the event manager. Parameters ---------- rest : hikari.api.rest.RestClient The Hikari REST client this will use. Other Parameters ---------------- cache : hikari.api.cache.CacheClient The Hikari cache client this will use if applicable. event_manager : hikari.api.event_manager.EventManagerClient The Hikari event manager client this will use if applicable. server : hikari.api.interaction_server.InteractionServer The Hikari interaction server client this will use if applicable. shards : hikari.traits.ShardAware The Hikari shard aware client this will use if applicable. voice : hikari.api.voice.VoiceComponent The Hikari voice component this will use if applicable. event_managed : bool Whether or not this client is managed by the event manager. An event managed client will be automatically started and closed based on Hikari's lifetime events. Defaults to `False` and can only be passed as `True` if `event_manager` is also provided. mention_prefix : bool Whether or not mention prefixes should be automatically set when this client is first started. Defaults to `False` and it should be noted that this only applies to message commands. declare_global_commands : typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool] Whether or not to automatically set global slash commands when this client is first started. Defaults to `False`. If one or more guild objects/IDs are passed here then the registered global commands will be set on the specified guild(s) at startup rather than globally. This can be useful for testing/debug purposes as slash commands may take up to an hour to propagate globally but will immediately propagate when set on a specific guild. set_global_commands : typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] Deprecated as of v2.1.1a1 alias of `declare_global_commands`. command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] If provided, a mapping of top level command names to IDs of the commands to update. This field is complementary to `declare_global_commands` and, while it isn't necessarily required, this will in some situations help avoid permissions which were previously set for a command from being lost after a rename. This currently isn't supported when multiple guild IDs are passed for `declare_global_commands`. Raises ------ ValueError Raises for the following reasons: * If `event_managed` is `True` when `event_manager` is `None`. * If `command_ids` is passed when multiple guild ids are provided for `declare_global_commands`. * If `command_ids` is passed when `declare_global_commands` is `False`. """ # noqa: E501 - line too long # InjectorClient.__init__ super().__init__() if _LOGGER.isEnabledFor(logging.INFO): _LOGGER.info( "%s initialised with the following components: %s", "Event-managed client" if event_managed else "Client", ", ".join( name for name, value in [ ("cache", cache), ("event manager", events), ("interaction server", server), ("rest", rest), ("shard manager", shards), ] if value ), ) if not events and not server: _LOGGER.warning( "Client initiaited without an event manager or interaction server, " "automatic command dispatch will be unavailable." ) self._accepts = MessageAcceptsEnum.ALL if events else MessageAcceptsEnum.NONE self._auto_defer_after: typing.Optional[float] = 2.0 self._cache = cache self._cached_application_id: typing.Optional[hikari.Snowflake] = None self._checks: list[checks.InjectableCheck] = [] self._client_callbacks: dict[str, list[injecting.CallbackDescriptor[None]]] = {} self._components: dict[str, tanjun_abc.Component] = {} self._defaults_to_ephemeral: bool = False self._make_message_context: _MessageContextMakerProto = context.MessageContext self._make_slash_context: _SlashContextMakerProto = context.SlashContext self._events = events self._grab_mention_prefix = mention_prefix self._hooks: typing.Optional[tanjun_abc.AnyHooks] = hooks.AnyHooks().set_on_parser_error(on_parser_error) self._interaction_not_found: typing.Optional[str] = "Command not found" self._slash_hooks: typing.Optional[tanjun_abc.SlashHooks] = None self._is_closing = False self._listeners: dict[type[hikari.Event], list[injecting.SelfInjectingCallback[None]]] = {} self._loop: typing.Optional[asyncio.AbstractEventLoop] = None self._message_hooks: typing.Optional[tanjun_abc.MessageHooks] = None self._metadata: dict[typing.Any, typing.Any] = {} self._modules: dict[str, types.ModuleType] = {} self._path_modules: dict[pathlib.Path, types.ModuleType] = {} self._prefix_getter: typing.Optional[injecting.CallbackDescriptor[collections.Iterable[str]]] = None self._prefixes: list[str] = [] self._rest = rest self._server = server self._shards = shards self._voice = voice if event_managed: if not events: raise ValueError("Client cannot be event managed without an event manager") events.subscribe(hikari.StartingEvent, self._on_starting_event) events.subscribe(hikari.StoppingEvent, self._on_stopping_event) if set_global_commands: warnings.warn( "The `set_global_commands` argument is deprecated since v2.1.1a1. " "Use `declare_global_commands` instead.", DeprecationWarning, stacklevel=2 + _stack_level, ) declare_global_commands = declare_global_commands or set_global_commands command_ids = command_ids or {} if isinstance(declare_global_commands, collections.Sequence): if command_ids and len(declare_global_commands) > 1: raise ValueError( "Cannot provide specific command_ids while automatically " "declaring commands marked as 'global' in multiple-guilds on startup" ) for guild in declare_global_commands: _LOGGER.info("Registering startup command declarer for %s guild", guild) self.add_client_callback(ClientCallbackNames.STARTING, _StartDeclarer(self, command_ids, guild)) elif isinstance(declare_global_commands, bool): if declare_global_commands: _LOGGER.info("Registering startup command declarer for global commands") if not command_ids: _LOGGER.warning( "No command IDs passed for startup command declarer, this could lead to previously set " "command permissions being lost when commands are renamed." ) self.add_client_callback( ClientCallbackNames.STARTING, _StartDeclarer(self, command_ids, hikari.UNDEFINED) ) elif command_ids: raise ValueError("Cannot pass command IDs when not declaring global commands") else: self.add_client_callback( ClientCallbackNames.STARTING, _StartDeclarer(self, command_ids, declare_global_commands) ) ( self.set_type_dependency(tanjun_abc.Client, self) .set_type_dependency(Client, self) .set_type_dependency(type(self), self) .set_type_dependency(hikari.api.RESTClient, rest) .set_type_dependency(type(rest), rest) ) if cache: self.set_type_dependency(hikari.api.Cache, cache).set_type_dependency(type(cache), cache) if events: self.set_type_dependency(hikari.api.EventManager, events).set_type_dependency(type(events), events) if server: self.set_type_dependency(hikari.api.InteractionServer, server).set_type_dependency(type(server), server) if shards: self.set_type_dependency(hikari_traits.ShardAware, shards).set_type_dependency(type(shards), shards) if voice: self.set_type_dependency(hikari.api.VoiceComponent, voice).set_type_dependency(type(voice), voice) dependencies.set_standard_dependencies(self)
Initialise a Tanjun client.
Notes
- For a quicker way to initiate this client around a standard bot aware
client, see
Client.from_gateway_botandClient.from_rest_bot. - The endpoint used by
declare_global_commandshas a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally). event_manageris necessary for message command dispatch and will also be necessary for interaction command dispatch ifserverisn't provided.serveris used for interaction command dispatch if interaction events aren't being received from the event manager.
Parameters
- rest (hikari.api.rest.RestClient): The Hikari REST client this will use.
Other Parameters
- cache (hikari.api.cache.CacheClient): The Hikari cache client this will use if applicable.
- event_manager (hikari.api.event_manager.EventManagerClient): The Hikari event manager client this will use if applicable.
- server (hikari.api.interaction_server.InteractionServer): The Hikari interaction server client this will use if applicable.
- shards (hikari.traits.ShardAware): The Hikari shard aware client this will use if applicable.
- voice (hikari.api.voice.VoiceComponent): The Hikari voice component this will use if applicable.
event_managed (bool): Whether or not this client is managed by the event manager.
An event managed client will be automatically started and closed based on Hikari's lifetime events.
Defaults to
Falseand can only be passed asTrueifevent_manageris also provided.mention_prefix (bool): Whether or not mention prefixes should be automatically set when this client is first started.
Defaults to
Falseand it should be noted that this only applies to message commands.declare_global_commands (typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool]): Whether or not to automatically set global slash commands when this client is first started. Defaults to
False.If one or more guild objects/IDs are passed here then the registered global commands will be set on the specified guild(s) at startup rather than globally. This can be useful for testing/debug purposes as slash commands may take up to an hour to propagate globally but will immediately propagate when set on a specific guild.
- set_global_commands (typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool]):
Deprecated as of v2.1.1a1 alias of
declare_global_commands. command_ids (typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]): If provided, a mapping of top level command names to IDs of the commands to update.
This field is complementary to
declare_global_commandsand, while it isn't necessarily required, this will in some situations help avoid permissions which were previously set for a command from being lost after a rename.This currently isn't supported when multiple guild IDs are passed for
declare_global_commands.
Raises
- ValueError: Raises for the following reasons:
- If
event_managedisTruewhenevent_managerisNone. - If
command_idsis passed when multiple guild ids are provided fordeclare_global_commands. - If
command_idsis passed whendeclare_global_commandsisFalse.
- If
View Source
@classmethod def from_gateway_bot( cls, bot: hikari_traits.GatewayBotAware, /, *, event_managed: bool = True, mention_prefix: bool = False, declare_global_commands: typing.Union[ hikari.SnowflakeishSequence[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool ] = False, set_global_commands: typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] = False, command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None, ) -> Client: """Build a `Client` from a `hikari.traits.GatewayBotAware` instance. Notes ----- * This implicitly defaults the client to human only mode. * This sets type dependency injectors for the hikari traits present in `bot` (including `hikari.traits.GatewayBotAware`). * The endpoint used by `declare_global_commands` has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally). Parameters ---------- bot : hikari.traits.GatewayBotAware The bot client to build from. This will be used to infer the relevant Hikari clients to use. Other Parameters ---------------- event_managed : bool Whether or not this client is managed by the event manager. An event managed client will be automatically started and closed based on Hikari's lifetime events. Defaults to `True`. mention_prefix : bool Whether or not mention prefixes should be automatically set when this client is first started. Defaults to `False` and it should be noted that this only applies to message commands. declare_global_commands : typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool] Whether or not to automatically set global slash commands when this client is first started. Defaults to `False`. If one or more guild objects/IDs are passed here then the registered global commands will be set on the specified guild(s) at startup rather than globally. This can be useful for testing/debug purposes as slash commands may take up to an hour to propagate globally but will immediately propagate when set on a specific guild. set_global_commands : typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] Deprecated as of v2.1.1a1 alias of `declare_global_commands`. command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] If provided, a mapping of top level command names to IDs of the commands to update. This field is complementary to `declare_global_commands` and, while it isn't necessarily required, this will in some situations help avoid permissions which were previously set for a command from being lost after a rename. This currently isn't supported when multiple guild IDs are passed for `declare_global_commands`. """ # noqa: E501 - line too long return ( cls( rest=bot.rest, cache=bot.cache, events=bot.event_manager, shards=bot, voice=bot.voice, event_managed=event_managed, mention_prefix=mention_prefix, declare_global_commands=declare_global_commands, set_global_commands=set_global_commands, command_ids=command_ids, _stack_level=1, ) .set_human_only() .set_hikari_trait_injectors(bot) )
Build a Client from a hikari.traits.GatewayBotAware instance.
Notes
- This implicitly defaults the client to human only mode.
- This sets type dependency injectors for the hikari traits present in
bot(includinghikari.traits.GatewayBotAware). - The endpoint used by
declare_global_commandshas a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally).
Parameters
bot (hikari.traits.GatewayBotAware): The bot client to build from.
This will be used to infer the relevant Hikari clients to use.
Other Parameters
event_managed (bool): Whether or not this client is managed by the event manager.
An event managed client will be automatically started and closed based on Hikari's lifetime events.
Defaults to
True.mention_prefix (bool): Whether or not mention prefixes should be automatically set when this client is first started.
Defaults to
Falseand it should be noted that this only applies to message commands.declare_global_commands (typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool]): Whether or not to automatically set global slash commands when this client is first started. Defaults to
False.If one or more guild objects/IDs are passed here then the registered global commands will be set on the specified guild(s) at startup rather than globally. This can be useful for testing/debug purposes as slash commands may take up to an hour to propagate globally but will immediately propagate when set on a specific guild.
- set_global_commands (typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool]):
Deprecated as of v2.1.1a1 alias of
declare_global_commands. command_ids (typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]): If provided, a mapping of top level command names to IDs of the commands to update.
This field is complementary to
declare_global_commandsand, while it isn't necessarily required, this will in some situations help avoid permissions which were previously set for a command from being lost after a rename.This currently isn't supported when multiple guild IDs are passed for
declare_global_commands.
View Source
@classmethod def from_rest_bot( cls, bot: hikari_traits.RESTBotAware, /, *, declare_global_commands: typing.Union[ hikari.SnowflakeishSequence[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool ] = False, set_global_commands: typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] = False, command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None, ) -> Client: """Build a `Client` from a `hikari.traits.RESTBotAware` instance. Notes ----- * This sets type dependency injectors for the hikari traits present in `bot` (including `hikari.traits.RESTBotAware`). * The endpoint used by `declare_global_commands` has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally). Parameters ---------- bot : hikari.traits.RESTBotAware The bot client to build from. Other Parameters ---------------- declare_global_commands : typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool] Whether or not to automatically set global slash commands when this client is first started. Defaults to `False`. If one or more guild objects/IDs are passed here then the registered global commands will be set on the specified guild(s) at startup rather than globally. This can be useful for testing/debug purposes as slash commands may take up to an hour to propagate globally but will immediately propagate when set on a specific guild. set_global_commands : typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool] Deprecated as of v2.1.1a1 alias of `declare_global_commands`. command_ids : typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] If provided, a mapping of top level command names to IDs of the commands to update. This field is complementary to `declare_global_commands` and, while it isn't necessarily required, this will in some situations help avoid permissions which were previously set for a command from being lost after a rename. This currently isn't supported when multiple guild IDs are passed for `declare_global_commands`. """ # noqa: E501 - line too long return cls( rest=bot.rest, server=bot.interaction_server, declare_global_commands=declare_global_commands, set_global_commands=set_global_commands, command_ids=command_ids, _stack_level=1, ).set_hikari_trait_injectors(bot)
Build a Client from a hikari.traits.RESTBotAware instance.
Notes
- This sets type dependency injectors for the hikari traits present in
bot(includinghikari.traits.RESTBotAware). - The endpoint used by
declare_global_commandshas a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally).
Parameters
- bot (hikari.traits.RESTBotAware): The bot client to build from.
Other Parameters
declare_global_commands (typing.Union[hikari.SnowflakeishSequenceOr[hikari.PartialGuild], hikari.SnowflakeishOr[hikari.PartialGuild], bool]): Whether or not to automatically set global slash commands when this client is first started. Defaults to
False.If one or more guild objects/IDs are passed here then the registered global commands will be set on the specified guild(s) at startup rather than globally. This can be useful for testing/debug purposes as slash commands may take up to an hour to propagate globally but will immediately propagate when set on a specific guild.
- set_global_commands (typing.Union[hikari.SnowflakeishOr[hikari.PartialGuild], bool]):
Deprecated as of v2.1.1a1 alias of
declare_global_commands. command_ids (typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]): If provided, a mapping of top level command names to IDs of the commands to update.
This field is complementary to
declare_global_commandsand, while it isn't necessarily required, this will in some situations help avoid permissions which were previously set for a command from being lost after a rename.This currently isn't supported when multiple guild IDs are passed for
declare_global_commands.
Whether slash contexts spawned by this client should default to ephemeral responses.
This effects calls to SlashContext.create_followup,
SlashContext.create_initial_response, SlashContext.defer and
SlashContext.respond unless the flags field is provided for the
methods which support it.
Notes
- This may be overridden by
BaseSlashCommand.defaults_to_ephemeralandComponent.defaults_to_ephemeral. - This defaults to
False. - This only effects slash command execution.
Type of message create events this command client accepts for execution.
Whether this client is only executing for non-bot/webhook users messages.
Hikari cache instance this command client was initialised with.
Collection of the level tanjun.abc.Context checks registered to this client.
Note: These may be taking advantage of the standard dependency injection.
Collection of the components this command client is using.
Object of the event manager this client was initialised with.
This is used for executing message commands if set.
Mapping of event types to the listeners registered in this client.
Top level tanjun.abc.AnyHooks set for this client.
These are called during both message and interaction command execution.
Returns
- typing.Optional[tanjun.abc.AnyHooks]: The top level
tanjun.abc.Contextbased hooks set for this client if applicable, elseNone.
Top level tanjun.abc.SlashHooks set for this client.
These are only called during interaction command execution.
Whether this client is alive.
The loop this client is bound to if it's alive.
Top level tanjun.abc.MessageHooks set for this client.
These are only called during both message command execution.
Mutable mapping of the metadata set for this client.
Note: Any modifications made to this mutable mapping will be preserved by the client.
Prefix getter method set for this client.
For more information on this callback's signature see PrefixGetter.
Collection of the standard prefixes set for this client.
Object of the Hikari REST client this client was initialised with.
Object of the Hikari interaction server provided for this client.
This is used for executing slash commands if set.
Object of the Hikari shard manager this client was initialised with.
Object of the Hikari voice component this client was initialised with.
View Source
async def clear_application_commands( self, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, ) -> None: # <<inherited docstring from tanjun.abc.Client>>. if application is None: application = self._cached_application_id or await self.fetch_rest_application_id() await self._rest.set_application_commands(application, (), guild=guild)
Clear the commands declared either globally or for a specific guild.
Note: The endpoint this uses has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally).
Other Parameters
application (typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialApplication]]): The application to clear commands for.
If left as
Nonethen this will be inferred from the authorization being used byClient.rest.guild (hikari.UndefinedOr[hikari.snowflakes.SnowflakeishOr[hikari.PartialGuild]]): Object or ID of the guild to clear commands for.
If left as
Noneglobal commands will be cleared.
View Source
async def set_global_commands( self, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, force: bool = False, ) -> collections.Sequence[hikari.Command]: """Alias of `Client.declare_global_commands`. .. deprecated:: v2.1.1a1 Use `Client.declare_global_commands` instead. """ warnings.warn( "The `Client.set_global_commands` method has been deprecated since v2.1.1a1. " "Use `Client.declare_global_commands` instead.", DeprecationWarning, stacklevel=2, ) return await self.declare_global_commands(application=application, guild=guild, force=force)
Alias of Client.declare_global_commands.
Deprecated since version v2.1.1a1:
Use Client.declare_global_commands instead.
View Source
async def declare_global_commands( self, command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, force: bool = False, ) -> collections.Sequence[hikari.Command]: # <<inherited docstring from tanjun.abc.Client>>. commands = ( command for command in itertools.chain.from_iterable( component.slash_commands for component in self._components.values() ) if command.is_global ) return await self.declare_application_commands( commands, command_ids, application=application, guild=guild, force=force )
Set the global application commands for a bot based on the loaded components.
Warning: This will overwrite any previously set application commands and only targets commands marked as global.
Notes
- The endpoint this uses has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally).
- Setting a specific
guildcan be useful for testing/debug purposes as slash commands may take up to an hour to propagate globally but will immediately propagate when set on a specific guild.
Other Parameters
- command_ids (typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]): If provided, a mapping of top level command names to IDs of the existing commands to update.
application (typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialApplication]]): Object or ID of the application to set the global commands for.
If left as
Nonethen this will be inferred from the authorization being used byClient.rest.guild (hikari.UndefinedOr[hikari.snowflakes.SnowflakeishOr[hikari.PartialGuild]]): Object or ID of the guild to set the global commands to.
If left as
Noneglobal commands will be set.force (bool): Force this to declare the commands regardless of whether or not they match the current state of the declared commands.
Defaults to
False. This default behaviour helps avoid issues with the 2 request per minute (per-guild or globally) ratelimit and the other limit of only 200 application command creates per day (per guild or globally).
Returns
- collections.abc.Sequence[hikari..Command]: API representations of the set commands.
View Source
async def declare_application_command( self, command: tanjun_abc.BaseSlashCommand, /, command_id: typing.Optional[hikari.Snowflakeish] = None, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, ) -> hikari.Command: # <<inherited docstring from tanjun.abc.Client>>. builder = command.build() if command_id: response = await self._rest.edit_application_command( application or self._cached_application_id or await self.fetch_rest_application_id(), command_id, guild=guild, name=builder.name, description=builder.description, options=builder.options, ) else: response = await self._rest.create_application_command( application or self._cached_application_id or await self.fetch_rest_application_id(), guild=guild, name=builder.name, description=builder.description, options=builder.options, ) if not guild: command.set_tracked_command(response) # TODO: is this fine? return response
Declare a single slash command for a bot.
Warning:
Providing command_id when updating a command helps avoid any
permissions set for the command being lose (e.g. when changing the
command's name).
Parameters
- command (BaseSlashCommand): The command to register.
Other Parameters
application (typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialApplication]]): The application to register the command with.
If left as
Nonethen this will be inferred from the authorization being used byClient.rest.- command_id (typing.Optional[hikari.snowflakes.Snowflakeish]): ID of the command to update.
guild (typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialGuild]]): Object or ID of the guild to register the command with.
If left as
Nonethen the command will be registered globally.
Returns
- hikari.Command: API representation of the command that was registered.
View Source
async def declare_application_commands( self, commands: collections.Iterable[tanjun_abc.BaseSlashCommand], /, command_ids: typing.Optional[collections.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]] = None, *, application: typing.Optional[hikari.SnowflakeishOr[hikari.PartialApplication]] = None, guild: hikari.UndefinedOr[hikari.SnowflakeishOr[hikari.PartialGuild]] = hikari.UNDEFINED, force: bool = False, ) -> collections.Sequence[hikari.Command]: # <<inherited docstring from tanjun.abc.Client>>. command_ids = command_ids or {} names_to_commands: dict[str, tanjun_abc.BaseSlashCommand] = {} conflicts: set[str] = set() builders: dict[str, hikari.api.CommandBuilder] = {} for command in commands: names_to_commands[command.name] = command if command.name in builders: conflicts.add(command.name) builder = command.build() if command_id := command_ids.get(command.name): builder.set_id(hikari.Snowflake(command_id)) builders[command.name] = builder if conflicts: raise ValueError( "Couldn't declare commands due to conflicts. The following command names have more than one command " "registered for them " + ", ".join(conflicts) ) if len(builders) > 100: raise ValueError("You can only declare up to 100 top level commands in a guild or globally") if not application: application = self._cached_application_id or await self.fetch_rest_application_id() target_type = "global" if guild is hikari.UNDEFINED else f"guild {int(guild)}" if not force: registered_commands = await self._rest.fetch_application_commands(application, guild=guild) if len(registered_commands) == len(builders) and all( _cmp_command(builders.get(command.name), command) for command in registered_commands ): _LOGGER.info("Skipping bulk declare for %s slash commands since they're already declared", target_type) return registered_commands _LOGGER.info("Bulk declaring %s %s slash commands", len(builders), target_type) responses = await self._rest.set_application_commands(application, list(builders.values()), guild=guild) for response in responses: if not guild: names_to_commands[response.name].set_tracked_command(response) # TODO: is this fine? if (expected_id := command_ids.get(response.name)) and hikari.Snowflake(expected_id) != response.id: _LOGGER.warning( "ID mismatch found for %s command %r, expected %s but got %s. " "This suggests that any previous permissions set for this command will have been lost.", target_type, response.name, expected_id, response.id, ) _LOGGER.info("Successfully declared %s (top-level) %s commands", len(responses), target_type) if _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug( "Declared %s command ids; %s", target_type, ", ".join(f"{response.name}: {response.id}" for response in responses), ) return responses
Declare a collection of slash commands for a bot.
Note: The endpoint this uses has a strict ratelimit which, as of writing, only allows for 2 requests per minute (with that ratelimit either being per-guild if targeting a specific guild otherwise globally).
Parameters
- commands (collections.abc.Iterable[BaseSlashCommand]): Iterable of the commands to register.
Other Parameters
command_ids (typing.Optional[collections.abc.Mapping[str, hikari.SnowflakeishOr[hikari.Command]]]): If provided, a mapping of top level command names to IDs of the existing commands to update.
While optional, this can be helpful when updating commands as providing the current IDs will prevent changes such as renames from leading to other state set for commands (e.g. permissions) from being lost.
application (typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialApplication]]): The application to register the commands with.
If left as
Nonethen this will be inferred from the authorization being used byClient.rest.guild (typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.PartialGuild]]): Object or ID of the guild to register the commands with.
If left as
Nonethen the commands will be registered globally.force (bool): Force this to declare the commands regardless of whether or not they match the current state of the declared commands.
Defaults to
False. This default behaviour helps avoid issues with the 2 request per minute (per-guild or globally) ratelimit and the other limit of only 200 application command creates per day (per guild or globally).
Returns
- collections.abc.Sequence[hikari.Command]: API representations of the commands which were registered.
Raises
- ValueError: Raises a value error for any of the following reasons:
- If conflicting command names are found (multiple commanbds have the same top-level name).
- If more than 100 top-level commands are passed.
View Source
def set_auto_defer_after(self: _ClientT, time: typing.Optional[float], /) -> _ClientT: """Set when this client should automatically defer execution of commands. .. warning:: If `time` is set to `None` then automatic deferrals will be disabled. This may lead to unexpected behaviour. Parameters ---------- time : typing.Optional[float] The time in seconds to defer interaction command responses after. """ self._auto_defer_after = float(time) if time is not None else None return self
Set when this client should automatically defer execution of commands.
Warning:
If time is set to None then automatic deferrals will be disabled.
This may lead to unexpected behaviour.
Parameters
- time (typing.Optional[float]): The time in seconds to defer interaction command responses after.
View Source
def set_ephemeral_default(self: _ClientT, state: bool, /) -> _ClientT: """Set whether slash contexts spawned by this client should default to ephemeral responses. Parameters ---------- bool Whether slash command contexts executed in this component should should default to ephemeral. This will be overridden by any response calls which specify flags and defaults to `False`. Returns ------- SelfT This component to enable method chaining. """ self._defaults_to_ephemeral = state return self
Set whether slash contexts spawned by this client should default to ephemeral responses.
Parameters
- bool: Whether slash command contexts executed in this component should should default to ephemeral.
This will be overridden by any response calls which specify flags
and defaults to False.
Returns
- SelfT: This component to enable method chaining.
View Source
def set_hikari_trait_injectors(self: _ClientT, bot: hikari_traits.RESTAware, /) -> _ClientT: """Set type based dependency injection based on the hikari traits found in `bot`. This is a short hand for calling `Client.add_type_dependency` for all the hikari trait types `bot` is valid for with bot. Parameters ---------- bot : hikari_traits.RESTAware The hikari client to set dependency injectors for. """ for _, member in inspect.getmembers(hikari_traits): if inspect.isclass(member) and isinstance(bot, member): self.set_type_dependency(member, bot) return self
Set type based dependency injection based on the hikari traits found in bot.
This is a short hand for calling Client.add_type_dependency for all
the hikari trait types bot is valid for with bot.
Parameters
- bot (hikari_traits.RESTAware): The hikari client to set dependency injectors for.
View Source
def set_interaction_not_found(self: _ClientT, message: typing.Optional[str], /) -> _ClientT: """Set the response message for when an interaction command is not found. .. warning:: Setting this to `None` may lead to unexpected behaviour (especially when the client is still set to auto-defer interactions) and should only be done if you know what you're doing. Parameters ---------- message : typing.Optional[str] The message to respond with when an interaction command isn't found. """ self._interaction_not_found = message return self
Set the response message for when an interaction command is not found.
Warning:
Setting this to None may lead to unexpected behaviour (especially
when the client is still set to auto-defer interactions) and should
only be done if you know what you're doing.
Parameters
- message (typing.Optional[str]): The message to respond with when an interaction command isn't found.
View Source
def set_message_accepts(self: _ClientT, accepts: MessageAcceptsEnum, /) -> _ClientT: """Set the kind of messages commands should be executed based on. Parameters ---------- accepts : MessageAcceptsEnum The type of messages commands should be executed based on. """ if accepts.get_event_type() and not self._events: raise ValueError("Cannot set accepts level on a client with no event manager") self._accepts = accepts return self
Set the kind of messages commands should be executed based on.
Parameters
- accepts (MessageAcceptsEnum): The type of messages commands should be executed based on.
View Source
def set_message_ctx_maker(self: _ClientT, maker: _MessageContextMakerProto = context.MessageContext, /) -> _ClientT: """Set the message context maker to use when creating context for a message. .. warning:: The caller must return an instance of `tanjun.context.MessageContext` rather than just any implementation of the MessageContext abc due to this client relying on implementation detail of `tanjun.context.MessageContext`. Parameters ---------- maker : _MessageContextMakerProto The message context maker to use. This is a callback which should match the signature of `tanjun.context.MessageContext.__init__` and return an instance of `tanjun.context.MessageContext`. This defaults to `tanjun.context.MessageContext`. """ self._make_message_context = maker return self
Set the message context maker to use when creating context for a message.
Warning:
The caller must return an instance of tanjun.context.MessageContext
rather than just any implementation of the MessageContext abc due to
this client relying on implementation detail of
tanjun.context.MessageContext.
Parameters
maker (_MessageContextMakerProto): The message context maker to use.
This is a callback which should match the signature of
tanjun.context.MessageContext.__init__and return an instance oftanjun.context.MessageContext.This defaults to
tanjun.context.MessageContext.
View Source
def set_metadata(self: _ClientT, key: typing.Any, value: typing.Any, /) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. self._metadata[key] = value return self
Set a field in the client's metadata.
Parameters
- key (typing.Any): Metadata key to set.
- value (typing.Any): Metadata value to set.
Returns
- Self: The client instance to enable chained calls.
View Source
def set_slash_ctx_maker(self: _ClientT, maker: _SlashContextMakerProto = context.SlashContext, /) -> _ClientT: """Set the slash context maker to use when creating context for a slash command. .. warning:: The caller must return an instance of `tanjun.context.SlashContext` rather than just any implementation of the SlashContext abc due to this client relying on implementation detail of `tanjun.context.SlashContext`. Parameters ---------- maker : _SlashContextMakerProto The slash context maker to use. This is a callback which should match the signature of `tanjun.context.SlashContext.__init__` and return an instance of `tanjun.context.SlashContext`. This defaults to `tanjun.context.SlashContext`. """ self._make_slash_context = maker return self
Set the slash context maker to use when creating context for a slash command.
Warning:
The caller must return an instance of tanjun.context.SlashContext
rather than just any implementation of the SlashContext abc due to
this client relying on implementation detail of
tanjun.context.SlashContext.
Parameters
maker (_SlashContextMakerProto): The slash context maker to use.
This is a callback which should match the signature of
tanjun.context.SlashContext.__init__and return an instance oftanjun.context.SlashContext.This defaults to
tanjun.context.SlashContext.
View Source
def set_human_only(self: _ClientT, value: bool = True) -> _ClientT: """Set whether or not message commands execution should be limited to "human" users. .. note:: This doesn't apply to interaction commands as these can only be triggered by a "human" (normal user account). Parameters ---------- value : bool Whether or not message commands execution should be limited to "human" users. Passing `True` here will prevent message commands from being executed based on webhook and bot messages. """ if value: self.add_check(_check_human) else: try: self.remove_check(_check_human) except ValueError: pass return self
Set whether or not message commands execution should be limited to "human" users.
Note: This doesn't apply to interaction commands as these can only be triggered by a "human" (normal user account).
Parameters
value (bool): Whether or not message commands execution should be limited to "human" users.
Passing
Truehere will prevent message commands from being executed based on webhook and bot messages.
View Source
def add_check(self: _ClientT, check: tanjun_abc.CheckSig, /) -> _ClientT: """Add a generic check to this client. This will be applied to both message and slash command execution. Parameters ---------- check : tanjun_abc.CheckSig The check to add. This may be either synchronous or asynchronous and must take one positional argument of type `tanjun.abc.Context` with dependency injection being supported for its keyword arguments. Returns ------- Self The client instance to enable chained calls. """ if check not in self._checks: self._checks.append(checks.InjectableCheck(check)) return self
Add a generic check to this client.
This will be applied to both message and slash command execution.
Parameters
- check (tanjun_abc.CheckSig):
The check to add. This may be either synchronous or asynchronous
and must take one positional argument of type
tanjun.abc.Contextwith dependency injection being supported for its keyword arguments.
Returns
- Self: The client instance to enable chained calls.
View Source
def remove_check(self: _ClientT, check: tanjun_abc.CheckSig, /) -> _ClientT: """Remove a check from the client. Parameters ---------- check : tanjun_abc.CheckSig The check to remove. Raises ------ ValueError If the check was not previously added. """ self._checks.remove(typing.cast("checks.InjectableCheck", check)) return self
Remove a check from the client.
Parameters
- check (tanjun_abc.CheckSig): The check to remove.
Raises
- ValueError: If the check was not previously added.
View Source
def with_check(self, check: tanjun_abc.CheckSigT, /) -> tanjun_abc.CheckSigT: """Add a check to this client through a decorator call. Parameters ---------- check : tanjun_abc.CheckSig The check to add. This may be either synchronous or asynchronous and must take one positional argument of type `tanjun.abc.Context` with dependency injection being supported for its keyword arguments. Returns ------- tanjun_abc.CheckSig The added check. """ self.add_check(check) return check
Add a check to this client through a decorator call.
Parameters
- check (tanjun_abc.CheckSig):
The check to add. This may be either synchronous or asynchronous
and must take one positional argument of type
tanjun.abc.Contextwith dependency injection being supported for its keyword arguments.
Returns
- tanjun_abc.CheckSig: The added check.
View Source
async def check(self, ctx: tanjun_abc.Context, /) -> bool: return await utilities.gather_checks(ctx, self._checks)
View Source
def add_component(self: _ClientT, component: tanjun_abc.Component, /, *, add_injector: bool = False) -> _ClientT: """Add a component to this client. Parameters ---------- component: Component The component to move to this client. Returns ------- Self The client instance to allow chained calls. Raises ------ ValueError If the component's name is already registered. """ if component.name in self._components: raise ValueError(f"A component named {component.name!r} is already registered.") component.bind_client(self) self._components[component.name] = component if add_injector: self.set_type_dependency(type(component), lambda: component) if self._loop: self._loop.create_task(component.open()) self._loop.create_task(self.dispatch_client_callback(ClientCallbackNames.COMPONENT_ADDED, component)) return self
Add a component to this client.
Parameters
- component (Component): The component to move to this client.
Returns
- Self: The client instance to allow chained calls.
Raises
- ValueError: If the component's name is already registered.
View Source
def get_component_by_name(self, name: str, /) -> typing.Optional[tanjun_abc.Component]: # <<inherited docstring from tanjun.abc.Client>>. return self._components.get(name)
Get a component from this client by name.
Parameters
- name (str): Name to get a component by.
Returns
- typing.Optional[Component]: The component instance if found, else
None.
View Source
def remove_component(self: _ClientT, component: tanjun_abc.Component, /) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. stored_component = self._components.get(component.name) if not stored_component or stored_component != component: raise ValueError(f"The component {component!r} is not registered.") del self._components[component.name] if self._loop: self._loop.create_task(component.close(unbind=True)) self._loop.create_task( self.dispatch_client_callback(ClientCallbackNames.COMPONENT_REMOVED, stored_component) ) else: stored_component.unbind_client(self) return self
Remove a component from this client.
This will unsubscribe any client callbacks, commands and listeners registered in the provided component.
Parameters
- component (Component): The component to remove from this client.
Raises
- ValueError: If the provided component isn't found.
Returns
- Self: The client instance to allow chained calls.
View Source
def remove_component_by_name(self: _ClientT, name: str, /) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. return self.remove_component(self._components[name])
Remove a component from this client by name.
This will unsubscribe any client callbacks, commands and listeners registered in the provided component.
Parameters
- name (str): Name of the component to remove from this client.
Raises
- KeyError: If the provided component name isn't found.
View Source
def add_client_callback( self: _ClientT, name: typing.Union[str, tanjun_abc.ClientCallbackNames], callback: tanjun_abc.MetaEventSig, / ) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. descriptor = injecting.CallbackDescriptor(callback) name = name.casefold() try: if descriptor in self._client_callbacks[name]: return self self._client_callbacks[name].append(descriptor) except KeyError: self._client_callbacks[name] = [descriptor] return self
Add a client callback.
Parameters
name (typing.Union[str, ClientCallbackNames]): The name this callback is being registered to.
This is case-insensitive.
callback (MetaEventSigT): The callback to register.
This may be sync or async and must return None. The positional and keyword arguments a callback should expect depend on implementation detail around the
namebeing subscribed to.
Returns
- Self: The client instance to enable chained calls.
View Source
async def dispatch_client_callback( self, name: typing.Union[str, tanjun_abc.ClientCallbackNames], /, *args: typing.Any ) -> None: # <<inherited docstring from tanjun.abc.Client>>. name = name.casefold() if callbacks := self._client_callbacks.get(name): calls = ( _wrap_client_callback(callback, injecting.BasicInjectionContext(self), args) for callback in callbacks ) await asyncio.gather(*calls)
Dispatch a client callback.
Parameters
- name (typing.Union[str, ClientCallbackNames]): The name of the callback to dispatch.
Other Parameters
- *args (typing.Any): Positional arguments to pass to the callback(s).
Raises
- KeyError: If no callbacks are registered for the given name.
View Source
def get_client_callbacks( self, name: typing.Union[str, tanjun_abc.ClientCallbackNames], / ) -> collections.Collection[tanjun_abc.MetaEventSig]: # <<inherited docstring from tanjun.abc.Client>>. name = name.casefold() if result := self._client_callbacks.get(name): return tuple(callback.callback for callback in result) return ()
Get a collection of the callbacks registered for a specific name.
Parameters
name (typing.Union[str, ClientCallbackNames]): The name to get the callbacks registered for.
This is case-insensitive.
Returns
- collections.abc.Collection[MetaEventSig]: Collection of the callbacks for the provided name.
View Source
def remove_client_callback( self: _ClientT, name: typing.Union[str, tanjun_abc.ClientCallbackNames], callback: tanjun_abc.MetaEventSig, / ) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. name = name.casefold() self._client_callbacks[name].remove(typing.cast("injecting.CallbackDescriptor[None]", callback)) if not self._client_callbacks[name]: del self._client_callbacks[name] return self
Remove a client callback.
Parameters
name (typing.Union[str, ClientCallbackNames]): The name this callback is being registered to.
This is case-insensitive.
- callback (MetaEventSigT): The callback to remove from the client's callbacks.
Raises
- KeyError: If the provided name isn't found.
- ValueError: If the provided callback isn't found.
Returns
- Self: The client instance to enable chained calls.
View Source
def with_client_callback( self, name: typing.Union[str, tanjun_abc.ClientCallbackNames], / ) -> collections.Callable[[tanjun_abc.MetaEventSigT], tanjun_abc.MetaEventSigT]: # <<inherited docstring from tanjun.abc.Client>>. def decorator(callback: tanjun_abc.MetaEventSigT, /) -> tanjun_abc.MetaEventSigT: self.add_client_callback(name, callback) return callback return decorator
Add a client callback through a decorator call.
Examples
client = tanjun.Client.from_rest_bot(bot)
@client.with_client_callback("closed")
async def on_close() -> None:
raise NotImplementedError
Parameters
name (typing.Union[str, ClientCallbackNames]): The name this callback is being registered to.
This is case-insensitive.
Returns
- collections.abc.Callable[[MetaEventSigT], MetaEventSigT]: Decorator callback used to register the client callback.
This may be sync or async and must return None. The positional and
keyword arguments a callback should expect depend on implementation
detail around the name being subscribed to.
View Source
def add_listener( self: _ClientT, event_type: type[hikari.Event], callback: tanjun_abc.ListenerCallbackSig, / ) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. injected: injecting.SelfInjectingCallback[None] = injecting.SelfInjectingCallback(self, callback) try: if callback in self._listeners[event_type]: return self self._listeners[event_type].append(injected) except KeyError: self._listeners[event_type] = [injected] if self._loop and self._events: self._events.subscribe(event_type, injected.__call__) return self
Add a listener to the client.
Parameters
- event_type (type[hikari.Event]): The event type to add a listener for.
callback (ListenerCallbackSig): The callback to register as a listener.
This callback must be a coroutine function which returns
Noneand always takes at least one positional arg of typehikari.Eventregardless of client implementation detail.
Returns
- Self: The client instance to enable chained calls.
View Source
def remove_listener( self: _ClientT, event_type: type[hikari.Event], callback: tanjun_abc.ListenerCallbackSig, / ) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. index = self._listeners[event_type].index(typing.cast("injecting.SelfInjectingCallback[None]", callback)) registered_callback = self._listeners[event_type].pop(index) if not self._listeners[event_type]: del self._listeners[event_type] if self._loop and self._events: self._events.unsubscribe(event_type, registered_callback.__call__) return self
Remove a listener from the client.
Parameters
- event_type (type[hikari.Event]): The event type to remove a listener for.
- callback (ListenerCallbackSig): The callback to remove.
Raises
- KeyError: If the provided event type isn't found.
- ValueError: If the provided callback isn't found.
Returns
- Self: The client instance to enable chained calls.
View Source
def with_listener( self, event_type: type[hikari.Event], / ) -> collections.Callable[[tanjun_abc.ListenerCallbackSigT], tanjun_abc.ListenerCallbackSigT]: # <<inherited docstring from tanjun.abc.Client>>. def decorator(callback: tanjun_abc.ListenerCallbackSigT, /) -> tanjun_abc.ListenerCallbackSigT: self.add_listener(event_type, callback) return callback return decorator
Add an event listener to this client through a decorator call.
Examples
client = tanjun.Client.from_gateway_bot(bot)
@client.with_listener(hikari.MessageCreateEvent)
async def on_message_create(event: hikari.MessageCreateEvent) -> None:
raise NotImplementedError
Parameters
- event_type (type[hikari.Event]): The event type to listener for.
Returns
- collections.abc.Callable[[ListenerCallbackSigT], ListenerCallbackSigT]: Decorator callback used to register the event callback.
The callback must be a coroutine function which returns None and
always takes at least one positional arg of type hikari.Event
regardless of client implementation detail.
View Source
def add_prefix(self: _ClientT, prefixes: typing.Union[collections.Iterable[str], str], /) -> _ClientT: """Add a prefix used to filter message command calls. This will be matched against the first character(s) in a message's content to determine whether the message command search stage of execution should be initiated. Parameters ---------- prefixes : typing.Union[collections.abc.Iterable[str], str] Either a single string or an iterable of strings to be used as prefixes. Returns ------- Self The client instance to enable chained calls. """ if isinstance(prefixes, str): if prefixes not in self._prefixes: self._prefixes.append(prefixes) else: self._prefixes.extend(prefix for prefix in prefixes if prefix not in self._prefixes) return self
Add a prefix used to filter message command calls.
This will be matched against the first character(s) in a message's content to determine whether the message command search stage of execution should be initiated.
Parameters
- prefixes (typing.Union[collections.abc.Iterable[str], str]): Either a single string or an iterable of strings to be used as prefixes.
Returns
- Self: The client instance to enable chained calls.
View Source
def remove_prefix(self: _ClientT, prefix: str, /) -> _ClientT: """Remove a message content prefix from the client. Parameters ---------- prefix : str The prefix to remove. Raises ------ ValueError If the prefix is not registered with the client. Returns ------- Self The client instance to enable chained calls. """ self._prefixes.remove(prefix) return self
Remove a message content prefix from the client.
Parameters
- prefix (str): The prefix to remove.
Raises
- ValueError: If the prefix is not registered with the client.
Returns
- Self: The client instance to enable chained calls.
View Source
def set_prefix_getter(self: _ClientT, getter: typing.Optional[PrefixGetterSig], /) -> _ClientT: """Set the callback used to retrieve message prefixes set for the relevant guild. Parameters ---------- getter : typing.Optional[PrefixGetterSig] The callback which'll be used to retrieve prefixes for the guild a message context is from. If `None` is passed here then the callback will be unset. This should be an async callback which one argument of type `tanjun.abc.MessageContext` and returns an iterable of string prefixes. Dependency injection is supported for this callback's keyword arguments. Returns ------- Self The client instance to enable chained calls. """ self._prefix_getter = injecting.CallbackDescriptor(getter) if getter else None return self
Set the callback used to retrieve message prefixes set for the relevant guild.
Parameters
getter (typing.Optional[PrefixGetterSig]): The callback which'll be used to retrieve prefixes for the guild a message context is from. If
Noneis passed here then the callback will be unset.This should be an async callback which one argument of type
tanjun.abc.MessageContextand returns an iterable of string prefixes. Dependency injection is supported for this callback's keyword arguments.
Returns
- Self: The client instance to enable chained calls.
View Source
def with_prefix_getter(self, getter: PrefixGetterSigT, /) -> PrefixGetterSigT: """Set the prefix getter callback for this client through decorator call. Examples -------- ```py client = tanjun.Client.from_rest_bot(bot) @client.with_prefix_getter async def prefix_getter(ctx: tanjun.abc.MessageContext) -> collections.abc.Iterable[str]: raise NotImplementedError ``` Parameters ---------- getter : PrefixGetterSig The callback which'll be to retrieve prefixes for the guild a message event is from. This should be an async callback which one argument of type `tanjun.abc.MessageContext` and returns an iterable of string prefixes. Dependency injection is supported for this callback's keyword arguments. Returns ------- PrefixGetterSigT The registered callback. """ self.set_prefix_getter(getter) return getter
Set the prefix getter callback for this client through decorator call.
Examples
client = tanjun.Client.from_rest_bot(bot)
@client.with_prefix_getter
async def prefix_getter(ctx: tanjun.abc.MessageContext) -> collections.abc.Iterable[str]:
raise NotImplementedError
Parameters
getter (PrefixGetterSig): The callback which'll be to retrieve prefixes for the guild a message event is from.
This should be an async callback which one argument of type
tanjun.abc.MessageContextand returns an iterable of string prefixes. Dependency injection is supported for this callback's keyword arguments.
Returns
- PrefixGetterSigT: The registered callback.
View Source
def iter_commands(self) -> collections.Iterator[tanjun_abc.ExecutableCommand[tanjun_abc.Context]]: # <<inherited docstring from tanjun.abc.Client>>. slash_commands = self.iter_slash_commands(global_only=False) yield from self.iter_message_commands() yield from slash_commands
Iterate over all the commands (both message and slash) registered to this client.
Returns
- collections.abc.Iterator[ExecutableCommand[Context]]: Iterator of all the commands registered to this client.
View Source
def iter_message_commands(self) -> collections.Iterator[tanjun_abc.MessageCommand[typing.Any]]: # <<inherited docstring from tanjun.abc.Client>>. return itertools.chain.from_iterable(component.message_commands for component in self.components)
Iterate over all the message commands registered to this client.
Returns
- collections.abc.Iterator[MessageCommand]: Iterator of all the message commands registered to this client.
View Source
def iter_slash_commands(self, *, global_only: bool = False) -> collections.Iterator[tanjun_abc.BaseSlashCommand]: # <<inherited docstring from tanjun.abc.Client>>. if global_only: return filter(lambda c: c.is_global, self.iter_slash_commands(global_only=False)) return itertools.chain.from_iterable(component.slash_commands for component in self.components)
Iterate over all the slash commands registered to this client.
Parameters
- global_only (bool): Whether to only iterate over global slash commands.
Returns
- collections.abc.Iterator[BaseSlashCommand]: Iterator of all the slash commands registered to this client.
View Source
def check_message_name( self, name: str, / ) -> collections.Iterator[tuple[str, tanjun_abc.MessageCommand[typing.Any]]]: # <<inherited docstring from tanjun.abc.Client>>. return itertools.chain.from_iterable( component.check_message_name(name) for component in self._components.values() )
Check whether a message command name is present in the current client.
Note: Dependent on implementation this may partial check name against the message command's name based on command_name.startswith(name).
Parameters
- name (str): The name to match commands against.
Returns
- collections.abc.Iterator[tuple[str, MessageCommand]]: Iterator of the matched command names to the matched message command objects.
View Source
def check_slash_name(self, name: str, /) -> collections.Iterator[tanjun_abc.BaseSlashCommand]: # <<inherited docstring from tanjun.abc.Client>>. return itertools.chain.from_iterable( component.check_slash_name(name) for component in self._components.values() )
Check whether a slash command name is present in the current client.
Note: This won't check the commands within command groups.
Parameters
- name (str): Name to check against.
Returns
- collections.abc.Iterator[BaseSlashCommand]: Iterator of the matched slash command objects.
View Source
async def close(self, *, deregister_listeners: bool = True) -> None: """Close the client. Raises ------ RuntimeError If the client isn't running. """ if not self._loop: raise RuntimeError("Client isn't active") if self._is_closing: event = asyncio.Event() self.add_client_callback(ClientCallbackNames.CLOSED, event.set) try: await event.wait() finally: self.remove_client_callback(ClientCallbackNames.CLOSED, event.set) return self._is_closing = True await self.dispatch_client_callback(ClientCallbackNames.CLOSING) if deregister_listeners and self._events: if event_type := self._accepts.get_event_type(): self._try_unsubscribe(self._events, event_type, self.on_message_create_event) self._try_unsubscribe(self._events, hikari.InteractionCreateEvent, self.on_interaction_create_event) for event_type_, listeners in self._listeners.items(): for listener in listeners: self._try_unsubscribe(self._events, event_type_, listener.__call__) if deregister_listeners and self._server: self._server.set_listener(hikari.CommandInteraction, None) await asyncio.gather(*(component.close() for component in self._components.copy().values())) self._loop = None await self.dispatch_client_callback(ClientCallbackNames.CLOSED) self._is_closing = False
Close the client.
Raises
- RuntimeError: If the client isn't running.
View Source
async def open(self, *, register_listeners: bool = True) -> None: """Start the client. If `mention_prefix` was passed to `Client.__init__` or `Client.from_gateway_bot` then this function may make a fetch request to Discord if it cannot get the current user from the cache. Raises ------ RuntimeError If the client is already active. """ if self._loop: raise RuntimeError("Client is already alive") self._loop = asyncio.get_running_loop() self._is_closing = False await self.dispatch_client_callback(ClientCallbackNames.STARTING) if self._grab_mention_prefix: user: typing.Optional[hikari.OwnUser] = None if self._cache: user = self._cache.get_me() if not user and (user_cache := self.get_type_dependency(dependencies.SingleStoreCache[hikari.OwnUser])): user = await user_cache.get(default=None) if not user: user = await self._rest.fetch_my_user() for prefix in f"<@{user.id}>", f"<@!{user.id}>": if prefix not in self._prefixes: self._prefixes.append(prefix) self._grab_mention_prefix = False await asyncio.gather(*(component.open() for component in self._components.copy().values())) if register_listeners and self._events: if event_type := self._accepts.get_event_type(): self._events.subscribe(event_type, self.on_message_create_event) self._events.subscribe(hikari.InteractionCreateEvent, self.on_interaction_create_event) for event_type_, listeners in self._listeners.items(): for listener in listeners: self._events.subscribe(event_type_, listener.__call__) if register_listeners and self._server: self._server.set_listener(hikari.CommandInteraction, self.on_interaction_create_request) self._loop.create_task(self.dispatch_client_callback(ClientCallbackNames.STARTED))
Start the client.
If mention_prefix was passed to Client.__init__ or
Client.from_gateway_bot then this function may make a fetch request
to Discord if it cannot get the current user from the cache.
Raises
- RuntimeError: If the client is already active.
View Source
async def fetch_rest_application_id(self) -> hikari.Snowflake: """Fetch the ID of the application this client is linked to. Returns ------- hikari.Snowflake The application ID of the application this client is linked to. """ if self._cached_application_id: return self._cached_application_id application_cache = self.get_type_dependency( dependencies.SingleStoreCache[hikari.Application] ) or self.get_type_dependency(dependencies.SingleStoreCache[hikari.AuthorizationApplication]) if application_cache and (application := await application_cache.get(default=None)): self._cached_application_id = application.id return application.id if self._rest.token_type == hikari.TokenType.BOT: self._cached_application_id = hikari.Snowflake(await self._rest.fetch_application()) else: self._cached_application_id = hikari.Snowflake((await self._rest.fetch_authorization()).application) return self._cached_application_id
Fetch the ID of the application this client is linked to.
Returns
- hikari.Snowflake: The application ID of the application this client is linked to.
View Source
def set_hooks(self: _ClientT, hooks: typing.Optional[tanjun_abc.AnyHooks], /) -> _ClientT: """Set the general command execution hooks for this client. The callbacks within this hook will be added to every slash and message command execution started by this client. Parameters ---------- hooks : typing.Optional[tanjun_abc.AnyHooks] The general command execution hooks to set for this client. Passing `None` will remove all hooks. Returns ------- Self The client instance to enable chained calls. """ self._hooks = hooks return self
Set the general command execution hooks for this client.
The callbacks within this hook will be added to every slash and message command execution started by this client.
Parameters
hooks (typing.Optional[tanjun_abc.AnyHooks]): The general command execution hooks to set for this client.
Passing
Nonewill remove all hooks.
Returns
- Self: The client instance to enable chained calls.
View Source
def set_slash_hooks(self: _ClientT, hooks: typing.Optional[tanjun_abc.SlashHooks], /) -> _ClientT: """Set the slash command execution hooks for this client. The callbacks within this hook will be added to every slash command execution started by this client. Parameters ---------- hooks : typing.Optional[tanjun_abc.SlashHooks] The slash context specific command execution hooks to set for this client. Passing `None` will remove the hooks. Returns ------- Self The client instance to enable chained calls. """ self._slash_hooks = hooks return self
Set the slash command execution hooks for this client.
The callbacks within this hook will be added to every slash command execution started by this client.
Parameters
hooks (typing.Optional[tanjun_abc.SlashHooks]): The slash context specific command execution hooks to set for this client.
Passing
Nonewill remove the hooks.
Returns
- Self: The client instance to enable chained calls.
View Source
def set_message_hooks(self: _ClientT, hooks: typing.Optional[tanjun_abc.MessageHooks], /) -> _ClientT: """Set the message command execution hooks for this client. The callbacks within this hook will be added to every message command execution started by this client. Parameters ---------- hooks : typing.Optional[tanjun_abc.MessageHooks] The message context specific command execution hooks to set for this client. Passing `None` will remove all hooks. Returns ------- Self The client instance to enable chained calls. """ self._message_hooks = hooks return self
Set the message command execution hooks for this client.
The callbacks within this hook will be added to every message command execution started by this client.
Parameters
hooks (typing.Optional[tanjun_abc.MessageHooks]): The message context specific command execution hooks to set for this client.
Passing
Nonewill remove all hooks.
Returns
- Self: The client instance to enable chained calls.
View Source
def load_modules(self: _ClientT, *modules: typing.Union[str, pathlib.Path]) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. for module_path in modules: if isinstance(module_path, pathlib.Path): module_path = module_path.absolute() generator = self._load_module(module_path) load_module = next(generator) with _WrapLoadError(errors.FailedModuleLoad): module = load_module() try: generator.send(module) except StopIteration: pass else: raise RuntimeError("Generator didn't finish") return self
Load entities into this client from modules based on present loaders.
Note:
If an __all__ is present in the target module then it will be
used to find loaders.
Examples
For this to work the target module has to have at least one loader present.
@tanjun.as_loader
def load_module(client: tanjun.Client) -> None:
client.add_component(component.copy())
or
loader = tanjun.Component("trans component").load_from_scope().make_loader()
Parameters
*modules (typing.Union[str, pathlib.Path]): Path(s) of the modules to load from.
When
strthis will be treated as a normal import path which is absolute ("foo.bar.baz"). It's worth noting that absolute module paths may be imported from the current location if the top level module is a valid module file or module directory in the current working directory.When
pathlib.Paththe module will be imported directly from the given path. In this mode any relative imports in the target module will fail to resolve.
Returns
- Self: This client instance to enable chained calls.
Raises
- tanjun.errors.FailedModuleLoad: If the new version of a module failed to load.
This includes if it failed to import or if one of its loaders raised.
The source error can be found at tanjun.errors.FailedModuleLoad.__source__.
- tanjun.errors.ModuleStateConflict: If the module is already loaded.
- tanjun.errors.ModuleMissingLoaders: If no loaders are found in the module.
- ModuleNotFoundError: If the module is not found.
View Source
async def load_modules_async(self, *modules: typing.Union[str, pathlib.Path]) -> None: # <<inherited docstring from tanjun.abc.Client>>. loop = asyncio.get_running_loop() for module_path in modules: if isinstance(module_path, pathlib.Path): module_path = await loop.run_in_executor(None, module_path.absolute) generator = self._load_module(module_path) load_module = next(generator) with _WrapLoadError(errors.FailedModuleLoad): module = await loop.run_in_executor(None, load_module) try: generator.send(module) except StopIteration: pass else: raise RuntimeError("Generator didn't finish")
Asynchronous variant of Client.load_modules.
Unlike Client.load_modules, this method will run blocking code in a
background thread.
For more information on the behaviour of this method see the
documentation for Client.load_modules.
View Source
def unload_modules(self: _ClientT, *modules: typing.Union[str, pathlib.Path]) -> _ClientT: # <<inherited docstring from tanjun.ab.Client>>. for module_path in modules: if isinstance(module_path, str): modules_dict: dict[typing.Any, types.ModuleType] = self._modules else: modules_dict = self._path_modules module_path = module_path.absolute() module = modules_dict.get(module_path) if not module: raise errors.ModuleStateConflict(f"Module {module_path!s} not loaded", module_path) _LOGGER.info("Unloading from %s", module_path) with _WrapLoadError(errors.FailedModuleUnload): self._call_unloaders(module_path, _get_loaders(module, module_path)) del modules_dict[module_path] return self
Unload entities from this client based on unloaders in one or more modules.
Note:
If an __all__ is present in the target module then it will be
used to find unloaders.
Examples
For this to work the module has to have at least one unloading enabled
tanjun.abc.ClientLoader present.
@tanjun.as_unloader
def unload_component(client: tanjun.Client) -> None:
client.remove_component_by_name(component.name)
or
# as_loader's returned ClientLoader handles both loading and unloading.
loader = tanjun.Component("trans component").load_from_scope().as_loader(unload_component)
Parameters
*modules (typing.Union[str, pathlib.Path]): Path of one or more modules to unload.
These should be the same path(s) which were passed to
load_module.
Returns
- Self: This client instance to enable chained calls.
Raises
- tanjun.errors.ModuleStateConflict: If the module hasn't been loaded.
- tanjun.errors.ModuleMissingLoaders: If no unloaders are found in the module.
- tanjun.errors.FailedModuleUnload: If the old version of a module failed to unload.
This indicates that one of its unloaders raised. The source
error can be found at tanjun.errors.FailedModuleUnload.__source__.
View Source
def reload_modules(self: _ClientT, *modules: typing.Union[str, pathlib.Path]) -> _ClientT: # <<inherited docstring from tanjun.abc.Client>>. for module_path in modules: if isinstance(module_path, pathlib.Path): module_path = module_path.absolute() generator = self._reload_module(module_path) load_module = next(generator) with _WrapLoadError(errors.FailedModuleLoad): module = load_module() try: generator.send(module) except StopIteration: pass else: raise RuntimeError("Generator didn't finish") return self
Reload entities in this client based on the loaders in loaded module(s).
Note:
If an __all__ is present in the target module then it will be
used to find loaders and unloaders.
Examples
For this to work the module has to have at least one ClientLoader which handles loading and one which handles unloading present.
Parameters
*modules (typing.Union[str, pathlib.Path]): Paths of one or more module to unload.
These should be the same paths which were passed to
load_module.
Returns
- Self: This client instance to enable chained calls.
Raises
- tanjun.errors.FailedModuleLoad: If the new version of a module failed to load.
This includes if it failed to import or if one of its loaders raised.
The source error can be found at tanjun.errors.FailedModuleLoad.__source__.
- tanjun.errors.FailedModuleUnload: If the old version of a module failed to unload.
This indicates that one of its unloaders raised. The source
error can be found at tanjun.errors.FailedModuleUnload.__source__.
- tanjun.errors.ModuleStateConflict: If the module hasn't been loaded.
- tanjun.errors.ModuleMissingLoaders: If no unloaders are found in the current state of the module. If no loaders are found in the new state of the module.
- ModuleNotFoundError: If the module can no-longer be found at the provided path.
View Source
async def reload_modules_async(self, *modules: typing.Union[str, pathlib.Path]) -> None: # <<inherited docstring from tanjun.abc.Client>>. loop = asyncio.get_running_loop() for module_path in modules: if isinstance(module_path, pathlib.Path): module_path = await loop.run_in_executor(None, module_path.absolute) generator = self._reload_module(module_path) load_module = next(generator) with _WrapLoadError(errors.FailedModuleLoad): module = await loop.run_in_executor(None, load_module) try: generator.send(module) except StopIteration: pass else: raise RuntimeError("Generator didn't finish")
Asynchronous variant of Client.reload_modules.
Unlike Client.reload_modules, this method will run blocking code in a
background thread.
For more information on the behaviour of this method see the
documentation for Client.reload_modules.
View Source
async def on_message_create_event(self, event: hikari.MessageCreateEvent, /) -> None: """Execute a message command based on a gateway event. Parameters ---------- hikari.events.message_events.MessageCreateEvent The event to handle. """ if event.message.content is None: return ctx = self._make_message_context( client=self, injection_client=self, content=event.message.content, message=event.message ) if (prefix := await self._check_prefix(ctx)) is None: return ctx.set_content(ctx.content.lstrip()[len(prefix) :].lstrip()).set_triggering_prefix(prefix) hooks: typing.Optional[set[tanjun_abc.MessageHooks]] = None if self._hooks and self._message_hooks: hooks = {self._hooks, self._message_hooks} elif self._hooks: hooks = {self._hooks} elif self._message_hooks: hooks = {self._message_hooks} try: if await self.check(ctx): for component in self._components.values(): if await component.execute_message(ctx, hooks=hooks): return except errors.HaltExecution: pass except errors.CommandError as exc: await ctx.respond(exc.message) return await self.dispatch_client_callback(ClientCallbackNames.MESSAGE_COMMAND_NOT_FOUND, ctx)
Execute a message command based on a gateway event.
Parameters
- hikari.events.message_events.MessageCreateEvent: The event to handle.
View Source
async def on_interaction_create_event(self, event: hikari.InteractionCreateEvent, /) -> None: """Execute a slash command based on Gateway events. .. note:: Any event where `event.interaction` is not `hikari.CommandInteraction` will be ignored. Parameters ---------- event : hikari.events.interaction_events.InteractionCreateEvent The event to execute commands based on. """ if not isinstance(event.interaction, hikari.CommandInteraction): return ctx = self._make_slash_context( client=self, injection_client=self, interaction=event.interaction, on_not_found=self._on_slash_not_found, default_to_ephemeral=self._defaults_to_ephemeral, ) hooks = self._get_slash_hooks() if self._auto_defer_after is not None: ctx.start_defer_timer(self._auto_defer_after) try: if await self.check(ctx): for component in self._components.values(): # This is set on each iteration to ensure that any component # state which was set to this isn't propagated to other components. ctx.set_ephemeral_default(self._defaults_to_ephemeral) if future := await component.execute_interaction(ctx, hooks=hooks): await future return except errors.HaltExecution: pass except errors.CommandError as exc: await ctx.respond(exc.message) return await ctx.mark_not_found()
Execute a slash command based on Gateway events.
Note:
Any event where event.interaction is not
hikari.CommandInteraction will be ignored.
Parameters
- event (hikari.events.interaction_events.InteractionCreateEvent): The event to execute commands based on.
View Source
async def on_interaction_create_request(self, interaction: hikari.CommandInteraction, /) -> context.ResponseTypeT: """Execute a slash command based on received REST requests. Parameters ---------- interaction : hikari.CommandInteraction The interaction to execute a command based on. Returns ------- tanjun.context.ResponseType The initial response to send back to Discord. """ ctx = self._make_slash_context( client=self, injection_client=self, interaction=interaction, on_not_found=self._on_slash_not_found, default_to_ephemeral=self._defaults_to_ephemeral, ) if self._auto_defer_after is not None: ctx.start_defer_timer(self._auto_defer_after) hooks = self._get_slash_hooks() future = ctx.get_response_future() try: if await self.check(ctx): for component in self._components.values(): # This is set on each iteration to ensure that any component # state which was set to this isn't propagated to other components. ctx.set_ephemeral_default(self._defaults_to_ephemeral) if await component.execute_interaction(ctx, hooks=hooks): return await future except errors.HaltExecution: pass except errors.CommandError as exc: # Under very specific timing there may be another future which could set a result while we await # ctx.respond therefore we create a task to avoid any erroneous behaviour from this trying to create # another response before it's returned the initial response. asyncio.get_running_loop().create_task( ctx.respond(exc.message), name=f"{interaction.id} command error responder" ) return await future asyncio.get_running_loop().create_task(ctx.mark_not_found(), name=f"{interaction.id} not found") return await future
Execute a slash command based on received REST requests.
Parameters
- interaction (hikari.CommandInteraction): The interaction to execute a command based on.
Returns
- tanjun.context.ResponseType: The initial response to send back to Discord.
View Source
class MessageAcceptsEnum(str, enum.Enum): """The possible configurations for which events `Client` should execute commands based on.""" ALL = "ALL" """Set the client to execute commands based on both DM and guild message create events.""" DM_ONLY = "DM_ONLY" """Set the client to execute commands based only DM message create events.""" GUILD_ONLY = "GUILD_ONLY" """Set the client to execute commands based only guild message create events.""" NONE = "NONE" """Set the client to not execute commands based on message create events.""" def get_event_type(self) -> typing.Optional[type[hikari.MessageCreateEvent]]: """Get the base event type this mode listens to. Returns ------- typing.Optional[type[hikari.message_events.MessageCreateEvent]] The type object of the MessageCreateEvent class this mode will register a listener for. This will be `None` if this mode disables listening to message create events. """ return _ACCEPTS_EVENT_TYPE_MAPPING[self]
The possible configurations for which events Client should execute commands based on.
Set the client to execute commands based on both DM and guild message create events.
Set the client to execute commands based only DM message create events.
Set the client to execute commands based only guild message create events.
Set the client to not execute commands based on message create events.
View Source
def get_event_type(self) -> typing.Optional[type[hikari.MessageCreateEvent]]: """Get the base event type this mode listens to. Returns ------- typing.Optional[type[hikari.message_events.MessageCreateEvent]] The type object of the MessageCreateEvent class this mode will register a listener for. This will be `None` if this mode disables listening to message create events. """ return _ACCEPTS_EVENT_TYPE_MAPPING[self]
Get the base event type this mode listens to.
Returns
- typing.Optional[type[hikari.message_events.MessageCreateEvent]]: The type object of the MessageCreateEvent class this mode will register a listener for.
This will be None if this mode disables listening to
message create events.
Inherited Members
- enum.Enum
- name
- value
- builtins.str
- encode
- replace
- split
- rsplit
- join
- capitalize
- casefold
- title
- center
- count
- expandtabs
- find
- partition
- index
- ljust
- lower
- lstrip
- rfind
- rindex
- rjust
- rstrip
- rpartition
- splitlines
- strip
- swapcase
- translate
- upper
- startswith
- endswith
- removeprefix
- removesuffix
- isascii
- islower
- isupper
- istitle
- isspace
- isdecimal
- isdigit
- isnumeric
- isalpha
- isalnum
- isidentifier
- isprintable
- zfill
- format
- format_map
- maketrans
View Source
# -*- coding: utf-8 -*- # cython: language_level=3 # BSD 3-Clause License # # Copyright (c) 2020-2022, Faster Speeding # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Standard implementation of Tanjun's command objects.""" from __future__ import annotations __all__: list[str] = [ "AnyMessageCommandT", "ConverterSig", "as_message_command", "as_message_command_group", "as_slash_command", "slash_command_group", "MessageCommand", "MessageCommandGroup", "PartialCommand", "BaseSlashCommand", "SlashCommand", "SlashCommandGroup", "with_str_slash_option", "with_int_slash_option", "with_float_slash_option", "with_bool_slash_option", "with_role_slash_option", "with_user_slash_option", "with_member_slash_option", "with_channel_slash_option", "with_mentionable_slash_option", ] import copy import re import typing import warnings from collections import abc as collections import hikari from . import abc from . import checks as checks_ from . import components from . import conversion from . import errors from . import hooks as hooks_ from . import injecting from . import utilities if typing.TYPE_CHECKING: from hikari.api import special_endpoints as special_endpoints_api _MessageCommandT = typing.TypeVar("_MessageCommandT", bound="MessageCommand[typing.Any]") _MessageCommandGroupT = typing.TypeVar("_MessageCommandGroupT", bound="MessageCommandGroup[typing.Any]") _PartialCommandT = typing.TypeVar("_PartialCommandT", bound="PartialCommand[typing.Any]") _BaseSlashCommandT = typing.TypeVar("_BaseSlashCommandT", bound="BaseSlashCommand") _SlashCommandT = typing.TypeVar("_SlashCommandT", bound="SlashCommand[typing.Any]") _SlashCommandGroupT = typing.TypeVar("_SlashCommandGroupT", bound="SlashCommandGroup") _CallbackishT = typing.Union[ abc.CommandCallbackSigT, abc.MessageCommand[abc.CommandCallbackSigT], abc.SlashCommand[abc.CommandCallbackSigT], ] AnyMessageCommandT = typing.TypeVar("AnyMessageCommandT", bound=abc.MessageCommand[typing.Any]) ConverterSig = collections.Callable[..., abc.MaybeAwaitableT[typing.Any]] """Type hint of a converter used for a slash command option.""" _EMPTY_DICT: typing.Final[dict[typing.Any, typing.Any]] = {} _EMPTY_HOOKS: typing.Final[hooks_.Hooks[typing.Any]] = hooks_.Hooks() class PartialCommand(abc.ExecutableCommand[abc.ContextT], components.AbstractComponentLoader): """Base class for the standard ExecutableCommand implementations.""" __slots__ = ("_checks", "_component", "_hooks", "_metadata") def __init__(self) -> None: self._checks: list[checks_.InjectableCheck] = [] self._component: typing.Optional[abc.Component] = None self._hooks: typing.Optional[abc.Hooks[abc.ContextT]] = None self._metadata: dict[typing.Any, typing.Any] = {} @property def checks(self) -> collections.Collection[abc.CheckSig]: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. return tuple(check.callback for check in self._checks) @property def component(self) -> typing.Optional[abc.Component]: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. return self._component @property def hooks(self) -> typing.Optional[abc.Hooks[abc.ContextT]]: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. return self._hooks @property def metadata(self) -> collections.MutableMapping[typing.Any, typing.Any]: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. return self._metadata @property def needs_injector(self) -> bool: # <<inherited docstring from tanjun.injecting.Injectable>>. return any(check.needs_injector for check in self._checks) def copy(self: _PartialCommandT, *, _new: bool = True) -> _PartialCommandT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. if not _new: self._checks = [check.copy() for check in self._checks] self._hooks = self._hooks.copy() if self._hooks else None self._metadata = self._metadata.copy() return self return copy.copy(self).copy(_new=False) def set_hooks(self: _PartialCommandT, hooks: typing.Optional[abc.Hooks[abc.ContextT]], /) -> _PartialCommandT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. self._hooks = hooks return self def set_metadata(self: _PartialCommandT, key: typing.Any, value: typing.Any, /) -> _PartialCommandT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. self._metadata[key] = value return self def add_check(self: _PartialCommandT, check: abc.CheckSig, /) -> _PartialCommandT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. if check not in self._checks: self._checks.append(checks_.InjectableCheck(check)) return self def remove_check(self: _PartialCommandT, check: abc.CheckSig, /) -> _PartialCommandT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. self._checks.remove(typing.cast("checks_.InjectableCheck", check)) return self def with_check(self, check: abc.CheckSigT, /) -> abc.CheckSigT: self.add_check(check) return check def bind_client(self: _PartialCommandT, client: abc.Client, /) -> _PartialCommandT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. return self def bind_component(self: _PartialCommandT, component: abc.Component, /) -> _PartialCommandT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. self._component = component return self _SCOMMAND_NAME_REG: typing.Final[re.Pattern[str]] = re.compile(r"^[\w-]{1,32}$", flags=re.UNICODE) def _validate_name(name: str) -> None: if not _SCOMMAND_NAME_REG.fullmatch(name): raise ValueError(f"Invalid name provided, {name!r} doesn't match the required regex `^\\w{{1,32}}$`") if name.lower() != name: raise ValueError(f"Invalid name provided, {name!r} must be lowercase") def slash_command_group( name: str, description: str, /, *, default_permission: bool = True, default_to_ephemeral: typing.Optional[bool] = None, is_global: bool = True, ) -> SlashCommandGroup: r"""Create a slash command group. Examples -------- Sub-commands can be added to the created slash command object through the following decorator based approach: ```python help_group = tanjun.slash_command_group("help", "get help") @help_group.with_command @tanjun.with_str_slash_option("command_name", "command name") @tanjun.as_slash_command("command", "Get help with a command") async def help_command_command(ctx: tanjun.abc.SlashContext, command_name: str) -> None: ... @help_group.with_command @tanjun.as_slash_command("me", "help me") async def help_me_command(ctx: tanjun.abc.SlashContext) -> None: ... component = tanjun.Component().add_slash_command(help_group) ``` Notes ----- * Unlike message command grups, slash command groups cannot be callable functions themselves. * Under the standard implementation, `is_global` is used to determine whether the command should be bulk set by `tanjun.Client.set_global_commands` or when `set_global_commands` is True Parameters ---------- name : str The name of the command group. This must match the regex `^[\w-]{1,32}$` in Unicode mode and be lowercase. description : str The description of the command group. Other Parameters ---------------- default_permission : bool Whether this command can be accessed without set permissions. Defaults to `True`, meaning that users can access the command by default. default_to_ephemeral : typing.Optional[bool] Whether this command's responses should default to ephemeral unless flags are set to override this. If this is left as `None` then the default set on the parent command(s), component or client will be in effect. is_global : bool Whether this command is a global command. Defaults to `True`. Returns ------- SlashCommandGroup The command group. Raises ------ ValueError Raises a value error for any of the following reasons: * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the command name has uppercase characters. * If the description is over 100 characters long. """ return SlashCommandGroup( name, description, default_permission=default_permission, default_to_ephemeral=default_to_ephemeral, is_global=is_global, ) def as_slash_command( name: str, description: str, /, *, always_defer: bool = False, default_permission: bool = True, default_to_ephemeral: typing.Optional[bool] = None, is_global: bool = True, sort_options: bool = True, ) -> collections.Callable[[_CallbackishT[abc.CommandCallbackSigT],], SlashCommand[abc.CommandCallbackSigT]]: r"""Build a `SlashCommand` by decorating a function. .. note:: Under the standard implementation, `is_global` is used to determine whether the command should be bulk set by `tanjun.Client.set_global_commands` or when `set_global_commands` is True .. warning:: `default_permission` and `is_global` are ignored for commands within slash command groups. Examples -------- ```py @as_slash_command("ping", "Get the bot's latency") async def ping_command(self, ctx: tanjun.abc.SlashContext) -> None: start_time = time.perf_counter() await ctx.rest.fetch_my_user() time_taken = (time.perf_counter() - start_time) * 1_000 await ctx.respond(f"PONG\n - REST: {time_taken:.0f}mss") ``` Parameters ---------- name : str The command's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The command's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- always_defer : bool Whether the contexts this command is executed with should always be deferred before being passed to the command's callback. Defaults to `False`. .. note:: The ephemeral state of the first response is decided by whether the deferral is ephemeral. default_permission : bool Whether this command can be accessed without set permissions. Defaults to `True`, meaning that users can access the command by default. default_to_ephemeral : typing.Optional[bool] Whether this command's responses should default to ephemeral unless flags are set to override this. If this is left as `None` then the default set on the parent command(s), component or client will be in effect. is_global : bool Whether this command is a global command. Defaults to `True`. sort_options : bool Whether this command should sort its set options based on whether they're required. If this is `True` then the options are re-sorted to meet the requirement from Discord that required command options be listed before optional ones. Returns ------- collections.abc.Callable[[_CallbackishT[CommandCallbackSigT]], SlashCommand[CommandCallbackSigT]] The decorator callback used to make a `SlashCommand`. This can either wrap a raw command callback or another callable command instance (e.g. `SlashCommand`, `MessageCommand`, `MessageCommandGroup`) and will manage loading the other command into a component when using `tanjun.Component.load_from_scope`. Raises ------ ValueError Raises a value error for any of the following reasons: * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the command name has uppercase characters. * If the description is over 100 characters long. """ def decorator(callback: _CallbackishT[abc.CommandCallbackSigT], /) -> SlashCommand[abc.CommandCallbackSigT]: if isinstance(callback, (abc.SlashCommand, abc.MessageCommand)): return SlashCommand( callback.callback, name, description, always_defer=always_defer, default_permission=default_permission, default_to_ephemeral=default_to_ephemeral, is_global=is_global, sort_options=sort_options, _wrapped_command=callback, ) return SlashCommand( callback, name, description, always_defer=always_defer, default_permission=default_permission, default_to_ephemeral=default_to_ephemeral, is_global=is_global, sort_options=sort_options, ) return decorator _UNDEFINED_DEFAULT = object() def with_str_slash_option( name: str, description: str, /, *, choices: typing.Union[collections.Mapping[str, str], collections.Sequence[str], None] = None, converters: typing.Union[collections.Sequence[ConverterSig], ConverterSig] = (), default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, ) -> collections.Callable[[_SlashCommandT], _SlashCommandT]: """Add a string option to a slash command. For more information on this function's parameters see `SlashCommand.add_str_option`. Examples -------- ```py @with_str_slash_option("name", "A name.") @as_slash_command("command", "A command") async def command(self, ctx: tanjun.abc.SlashContext, name: str) -> None: ... ``` Returns ------- collections.abc.Callable[[_SlashCommandT], _SlashCommandT] Decorator callback which adds the option to the command. """ return lambda c: c.add_str_option( name, description, default=default, choices=choices, converters=converters, pass_as_kwarg=pass_as_kwarg, _stack_level=1, ) def with_int_slash_option( name: str, description: str, /, *, choices: typing.Optional[collections.Mapping[str, int]] = None, converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (), default: typing.Any = _UNDEFINED_DEFAULT, min_value: typing.Optional[int] = None, max_value: typing.Optional[int] = None, pass_as_kwarg: bool = True, ) -> collections.Callable[[_SlashCommandT], _SlashCommandT]: """Add an integer option to a slash command. For information on this function's parameters see `SlashCommand.add_int_option`. Examples -------- ```py @with_int_slash_option("int_value", "Int value.") @as_slash_command("command", "A command") async def command(self, ctx: tanjun.abc.SlashContext, int_value: int) -> None: ... ``` Returns ------- collections.abc.Callable[[_SlashCommandT], _SlashCommandT] Decorator callback which adds the option to the command. """ return lambda c: c.add_int_option( name, description, default=default, choices=choices, converters=converters, min_value=min_value, max_value=max_value, pass_as_kwarg=pass_as_kwarg, _stack_level=1, ) def with_float_slash_option( name: str, description: str, /, *, always_float: bool = True, choices: typing.Optional[collections.Mapping[str, float]] = None, converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (), default: typing.Any = _UNDEFINED_DEFAULT, min_value: typing.Optional[float] = None, max_value: typing.Optional[float] = None, pass_as_kwarg: bool = True, ) -> collections.Callable[[_SlashCommandT], _SlashCommandT]: """Add a float option to a slash command. For information on this function's parameters see `SlashCommand.add_float_option`. Examples -------- ```py @with_float_slash_option("float_value", "Float value.") @as_slash_command("command", "A command") async def command(self, ctx: tanjun.abc.SlashContext, float_value: float) -> None: ... ``` Returns ------- collections.abc.Callable[[_SlashCommandT], _SlashCommandT] Decorator callback which adds the option to the command. """ return lambda c: c.add_float_option( name, description, always_float=always_float, default=default, choices=choices, converters=converters, min_value=min_value, max_value=max_value, pass_as_kwarg=pass_as_kwarg, _stack_level=1, ) def with_bool_slash_option( name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True ) -> collections.Callable[[_SlashCommandT], _SlashCommandT]: """Add a boolean option to a slash command. For information on this function's parameters see `SlashContext.add_bool_option`. Examples -------- ```py @with_bool_slash_option("flag", "Whether this flag should be enabled.", default=False) @as_slash_command("command", "A command") async def command(self, ctx: tanjun.abc.SlashContext, flag: bool) -> None: ... ``` Returns ------- collections.abc.Callable[[_SlashCommandT], _SlashCommandT] Decorator callback which adds the option to the command. """ return lambda c: c.add_bool_option(name, description, default=default, pass_as_kwarg=pass_as_kwarg) def with_user_slash_option( name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True ) -> collections.Callable[[_SlashCommandT], _SlashCommandT]: """Add a user option to a slash command. For information on this function's parameters see `SlashContext.add_user_option`. .. note:: This may result in `hikari.InteractionMember` or `hikari.users.User` if the user isn't in the current guild or if this command was executed in a DM channel. Examples -------- ```py @with_user_slash_option("user", "user to target.") @as_slash_command("command", "A command") async def command(self, ctx: tanjun.abc.SlashContext, user: Union[InteractionMember, User]) -> None: ... ``` Returns ------- collections.abc.Callable[[_SlashCommandT], _SlashCommandT] Decorator callback which adds the option to the command. """ return lambda c: c.add_user_option(name, description, default=default, pass_as_kwarg=pass_as_kwarg) def with_member_slash_option( name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT ) -> collections.Callable[[_SlashCommandT], _SlashCommandT]: """Add a member option to a slash command. For information on this function's arguments see `SlashCommand.add_member_option`. .. note:: This will always result in `hikari.InteractionMember`. Examples -------- ```py @with_member_slash_option("member", "member to target.") @as_slash_command("command", "A command") async def command(self, ctx: tanjun.abc.SlashContext, member: hikari.InteractionMember) -> None: ... ``` Returns ------- collections.abc.Callable[[_SlashCommandT], _SlashCommandT] Decorator callback which adds the option to the command. """ return lambda c: c.add_member_option(name, description, default=default) _channel_types: dict[type[hikari.PartialChannel], set[hikari.ChannelType]] = { hikari.GuildTextChannel: {hikari.ChannelType.GUILD_TEXT}, hikari.DMChannel: {hikari.ChannelType.DM}, hikari.GuildVoiceChannel: {hikari.ChannelType.GUILD_VOICE}, hikari.GroupDMChannel: {hikari.ChannelType.GROUP_DM}, hikari.GuildCategory: {hikari.ChannelType.GUILD_CATEGORY}, hikari.GuildNewsChannel: {hikari.ChannelType.GUILD_NEWS}, hikari.GuildStoreChannel: {hikari.ChannelType.GUILD_STORE}, hikari.GuildStageChannel: {hikari.ChannelType.GUILD_STAGE}, } for _channel_cls, _types in _channel_types.copy().items(): for _mro_type in _channel_cls.mro(): if isinstance(_mro_type, type) and issubclass(_mro_type, hikari.PartialChannel): try: _channel_types[_mro_type].update(_types) except KeyError: _channel_types[_mro_type] = _types.copy() def with_channel_slash_option( name: str, description: str, /, *, types: typing.Union[collections.Collection[type[hikari.PartialChannel]], None] = None, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, ) -> collections.Callable[[_SlashCommandT], _SlashCommandT]: """Add a channel option to a slash command. For information on this function's parameters see `SlashCommand.add_channel_option`. .. note:: This will always result in `hikari..InteractionChannel`. Examples -------- ```py @with_channel_slash_option("channel", "channel to target.") @as_slash_command("command", "A command") async def command(self, ctx: tanjun.abc.SlashContext, channel: hikari.InteractionChannel) -> None: ... ``` Returns ------- collections.abc.Callable[[_SlashCommandT], _SlashCommandT] Decorator callback which adds the option to the command. """ return lambda c: c.add_channel_option(name, description, types=types, default=default, pass_as_kwarg=pass_as_kwarg) def with_role_slash_option( name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True ) -> collections.Callable[[_SlashCommandT], _SlashCommandT]: """Add a role option to a slash command. For information on this function's parameters see `SlashCommand.add_role_option`. Examples -------- ```py @with_role_slash_option("role", "Role to target.") @as_slash_command("command", "A command") async def command(self, ctx: tanjun.abc.SlashContext, role: hikari.Role) -> None: ... ``` Returns ------- collections.abc.Callable[[_SlashCommandT], _SlashCommandT] Decorator callback which adds the option to the command. """ return lambda c: c.add_role_option(name, description, default=default, pass_as_kwarg=pass_as_kwarg) def with_mentionable_slash_option( name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True ) -> collections.Callable[[_SlashCommandT], _SlashCommandT]: """Add a mentionable option to a slash command. For information on this function's arguments see `SlashCommand.add_mentionable_option`. .. note:: This may target roles, guild members or users and results in `Union[hikari.User, hikari.InteractionMember, hikari.Role]`. Examples -------- ```py @with_mentionable_slash_option("mentionable", "Mentionable entity to target.") @as_slash_command("command", "A command") async def command(self, ctx: tanjun.abc.SlashContext, mentionable: [Role, InteractionMember, User]) -> None: ... ``` Returns ------- collections.abc.Callable[[_SlashCommandT], _SlashCommandT] Decorator callback which adds the option to the command. """ return lambda c: c.add_mentionable_option(name, description, default=default, pass_as_kwarg=pass_as_kwarg) def _convert_to_injectable(converter: ConverterSig) -> injecting.CallbackDescriptor[typing.Any]: if isinstance(converter, injecting.CallbackDescriptor): return typing.cast("injecting.CallbackDescriptor[typing.Any]", converter) return injecting.CallbackDescriptor(conversion.override_type(converter)) class _TrackedOption: __slots__ = ("converters", "default", "is_always_float", "is_only_member", "name", "type") def __init__( self, *, name: str, option_type: typing.Union[hikari.OptionType, int], always_float: bool = False, converters: typing.Optional[list[injecting.CallbackDescriptor[typing.Any]]] = None, only_member: bool = False, default: typing.Any = _UNDEFINED_DEFAULT, ) -> None: self.converters = converters or [] self.default = default self.is_always_float = always_float self.is_only_member = only_member self.name = name self.type = option_type @property def needs_injector(self) -> bool: return any(converter.needs_injector for converter in self.converters) def check_client(self, client: abc.Client, /) -> None: for converter in self.converters: if isinstance(converter.callback, conversion.BaseConverter): converter.callback.check_client(client, f"{self.name} slash command option") async def convert(self, ctx: abc.SlashContext, value: typing.Any, /) -> typing.Any: if not self.converters: return value exceptions: list[ValueError] = [] for converter in self.converters: try: return await converter.resolve_with_command_context(ctx, value) except ValueError as exc: exceptions.append(exc) raise errors.ConversionError(f"Couldn't convert {self.type} '{self.name}'", self.name, errors=exceptions) _CommandBuilderT = typing.TypeVar("_CommandBuilderT", bound="_CommandBuilder") class _CommandBuilder(hikari.impl.CommandBuilder): __slots__ = ("_has_been_sorted", "_sort_options") def __init__( self, name: str, description: str, sort_options: bool, *, id: hikari.UndefinedOr[hikari.Snowflake] = hikari.UNDEFINED, # noqa: A002 ) -> None: super().__init__(name, description, id=id) # type: ignore self._has_been_sorted = True self._sort_options = sort_options def add_option(self: _CommandBuilderT, option: hikari.CommandOption) -> _CommandBuilderT: if self._options: self._has_been_sorted = False super().add_option(option) return self def sort(self: _CommandBuilderT) -> _CommandBuilderT: if self._sort_options and not self._has_been_sorted: required: list[hikari.CommandOption] = [] not_required: list[hikari.CommandOption] = [] for option in self._options: if option.is_required: required.append(option) else: not_required.append(option) self._options = [*required, *not_required] self._has_been_sorted = True return self def copy(self) -> _CommandBuilder: # TODO: can we just del _CommandBuilder.__copy__ to go back to the default? builder = _CommandBuilder(self.name, self.description, self._sort_options, id=self.id) for option in self._options: builder.add_option(option) return builder class BaseSlashCommand(PartialCommand[abc.SlashContext], abc.BaseSlashCommand): """Base class used for the standard slash command implementations.""" __slots__ = ("_defaults_to_ephemeral", "_description", "_is_global", "_name", "_parent", "_tracked_command") def __init__( self, name: str, description: str, /, *, default_to_ephemeral: typing.Optional[bool] = None, is_global: bool = True, ) -> None: super().__init__() _validate_name(name) if len(description) > 100: raise ValueError("The command description cannot be over 100 characters in length") self._defaults_to_ephemeral = default_to_ephemeral self._description = description self._is_global = is_global self._name = name self._parent: typing.Optional[abc.SlashCommandGroup] = None self._tracked_command: typing.Optional[hikari.Command] = None @property def defaults_to_ephemeral(self) -> typing.Optional[bool]: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. return self._defaults_to_ephemeral @property def description(self) -> str: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. return self._description @property def is_global(self) -> bool: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. return self._is_global @property def name(self) -> str: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. return self._name @property def parent(self) -> typing.Optional[abc.SlashCommandGroup]: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. return self._parent @property def tracked_command(self) -> typing.Optional[hikari.Command]: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. return self._tracked_command @property def tracked_command_id(self) -> typing.Optional[hikari.Snowflake]: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. return self._tracked_command.id if self._tracked_command else None def set_tracked_command(self: _BaseSlashCommandT, command: hikari.Command, /) -> _BaseSlashCommandT: """Set the the global command this should be tracking. Parameters ---------- command : hikari.Command object of the global command this should be tracking. Returns ------- SelfT This command instance for chaining. """ self._tracked_command = command return self def set_ephemeral_default(self: _BaseSlashCommandT, state: typing.Optional[bool], /) -> _BaseSlashCommandT: """Set whether this command's responses should default to ephemeral. Parameters ---------- typing.Optional[bool] Whether this command's responses should default to ephemeral. This will be overridden by any response calls which specify flags. Setting this to `None` will let the default set on the parent command(s), component or client propagate and decide the ephemeral default for contexts used by this command. Returns ------- SelfT This command to allow for chaining. """ self._defaults_to_ephemeral = state return self def set_parent(self: _BaseSlashCommandT, parent: typing.Optional[abc.SlashCommandGroup], /) -> _BaseSlashCommandT: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. self._parent = parent return self async def check_context(self, ctx: abc.SlashContext, /) -> bool: # <<inherited docstring from tanjun.abc.SlashCommand>>. ctx.set_command(self) result = await utilities.gather_checks(ctx, self._checks) ctx.set_command(None) return result def copy( self: _BaseSlashCommandT, *, _new: bool = True, parent: typing.Optional[abc.SlashCommandGroup] = None ) -> _BaseSlashCommandT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. if not _new: self._parent = parent return super().copy(_new=_new) return super().copy(_new=_new) def load_into_component(self, component: abc.Component, /) -> None: # <<inherited docstring from tanjun.components.load_into_component>>. if not self._parent: component.add_slash_command(self) class SlashCommandGroup(BaseSlashCommand, abc.SlashCommandGroup): """Standard implementation of a slash command group. .. note:: Unlike message command grups, slash command groups cannot be callable functions themselves. """ __slots__ = ("_commands", "_default_permission") def __init__( self, name: str, description: str, /, *, default_to_ephemeral: typing.Optional[bool] = None, default_permission: bool = True, is_global: bool = True, ) -> None: r"""Initialise a slash command group. .. note:: Under the standard implementation, `is_global` is used to determine whether the command should be bulk set by `tanjun.Client.set_global_commands` or when `set_global_commands` is True Parameters ---------- name : str The name of the command group. This must match the regex `^[\w-]{1,32}$` in Unicode mode and be lowercase. description : str The description of the command group. Other Parameters ---------------- default_permission : bool Whether this command can be accessed without set permissions. Defaults to `True`, meaning that users can access the command by default. default_to_ephemeral : typing.Optional[bool] Whether this command's responses should default to ephemeral unless flags are set to override this. If this is left as `None` then the default set on the parent command(s), component or client will be in effect. is_global : bool Whether this command is a global command. Defaults to `True`. Raises ------ ValueError Raises a value error for any of the following reasons: * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the command name has uppercase characters. * If the description is over 100 characters long. """ super().__init__(name, description, default_to_ephemeral=default_to_ephemeral, is_global=is_global) self._commands: dict[str, abc.BaseSlashCommand] = {} self._default_permission = default_permission @property def commands(self) -> collections.Collection[abc.BaseSlashCommand]: # <<inherited docstring from tanjun.abc.SlashCommandGroup>>. return self._commands.copy().values() def build(self) -> special_endpoints_api.CommandBuilder: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. builder = _CommandBuilder(self._name, self._description, False).set_default_permission(self._default_permission) for command in self._commands.values(): option_type = ( hikari.OptionType.SUB_COMMAND_GROUP if isinstance(command, abc.SlashCommandGroup) else hikari.OptionType.SUB_COMMAND ) command_builder = command.build() builder.add_option( hikari.CommandOption( type=option_type, name=command.name, description=command_builder.description, is_required=False, options=command_builder.options, ) ) return builder def copy( self: _SlashCommandGroupT, *, _new: bool = True, parent: typing.Optional[abc.SlashCommandGroup] = None ) -> _SlashCommandGroupT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. if not _new: self._commands = {name: command.copy() for name, command in self._commands.items()} return super().copy(_new=_new, parent=parent) return super().copy(_new=_new, parent=parent) def add_command(self: _SlashCommandGroupT, command: abc.BaseSlashCommand, /) -> _SlashCommandGroupT: """Add a slash command to this group. .. warning:: Command groups are only supported within top-level groups. Parameters ---------- command : tanjun.abc.BaseSlashCommand Command to add to this group. Returns ------- Self Object of this group to enable chained calls. """ if self._parent and isinstance(command, abc.SlashCommandGroup): raise ValueError("Cannot add a slash command group to a nested slash command group") if len(self._commands) == 25: raise ValueError("Cannot add more than 25 commands to a slash command group") if command.name in self._commands: raise ValueError(f"Command with name {command.name!r} already exists in this group") command.set_parent(self) self._commands[command.name] = command return self def remove_command(self: _SlashCommandGroupT, command: abc.BaseSlashCommand, /) -> _SlashCommandGroupT: """Remove a command from this group. Parameters ---------- command : tanjun.abc.BaseSlashCommand Command to remove from this group. Returns ------- Self Object of this group to enable chained calls. """ del self._commands[command.name] return self def with_command(self, command: abc.BaseSlashCommandT, /) -> abc.BaseSlashCommandT: """Add a slash command to this group through a decorator call. Parameters ---------- command : tanjun.abc.BaseSlashCommand Command to add to this group. Returns ------- tanjun.abc.BaseSlashCommand Command which was added to this group. """ self.add_command(command) return command async def execute( self, ctx: abc.SlashContext, /, option: typing.Optional[hikari.CommandInteractionOption] = None, *, hooks: typing.Optional[collections.MutableSet[abc.SlashHooks]] = None, ) -> None: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. if not option and ctx.interaction.options: option = ctx.interaction.options[0] elif option and option.options: option = option.options[0] else: raise RuntimeError("Missing sub-command option") if command := self._commands.get(option.name): if command.defaults_to_ephemeral is not None: ctx.set_ephemeral_default(command.defaults_to_ephemeral) if await command.check_context(ctx): await command.execute(ctx, option=option, hooks=hooks) return await ctx.mark_not_found() class SlashCommand(BaseSlashCommand, abc.SlashCommand[abc.CommandCallbackSigT]): """Standard implementation of a slash command.""" __slots__ = ("_always_defer", "_builder", "_callback", "_client", "_tracked_options", "_wrapped_command") def __init__( self, callback: abc.CommandCallbackSigT, name: str, description: str, /, *, always_defer: bool = False, default_permission: bool = True, default_to_ephemeral: typing.Optional[bool] = None, is_global: bool = True, sort_options: bool = True, _wrapped_command: typing.Optional[abc.ExecutableCommand[typing.Any]] = None, ) -> None: r"""Initialise a slash command. .. note:: Under the standard implementation, `is_global` is used to determine whether the command should be bulk set by `tanjun.Client.set_global_commands` or when `set_global_commands` is True .. warning:: `default_permission` and `is_global` are ignored for commands within slash command groups. Parameters ---------- callback : collections.abc.Callable[[tanjun.abc.SlashContext, ...], collections.abc.Awaitable[None]] Callback to execute when the command is invoked. This should be an asynchronous callback which takes one positional argument of type `tanjun.abc.SlashContext`, returns `None` and may use dependency injection to access other services. name : str The command's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The command's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- always_defer : bool Whether the contexts this command is executed with should always be deferred before being passed to the command's callback. Defaults to `False`. .. note:: The ephemeral state of the first response is decided by whether the deferral is ephemeral. default_permission : bool Whether this command can be accessed without set permissions. Defaults to `True`, meaning that users can access the command by default. default_to_ephemeral : typing.Optional[bool] Whether this command's responses should default to ephemeral unless flags are set to override this. If this is left as `None` then the default set on the parent command(s), component or client will be in effect. is_global : bool Whether this command is a global command. Defaults to `True`. sort_options : bool Whether this command should sort its set options based on whether they're required. If this is `True` then the options are re-sorted to meet the requirement from Discord that required command options be listed before optional ones. Raises ------ ValueError Raises a value error for any of the following reasons: * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the command name has uppercase characters. * If the description is over 100 characters long. """ super().__init__(name, description, default_to_ephemeral=default_to_ephemeral, is_global=is_global) if not _wrapped_command and isinstance(callback, (abc.MessageCommand, abc.SlashCommand)): callback = typing.cast(abc.CommandCallbackSigT, callback.callback) self._always_defer = always_defer self._builder = _CommandBuilder(name, description, sort_options).set_default_permission(default_permission) self._callback = injecting.CallbackDescriptor[None](callback) self._client: typing.Optional[abc.Client] = None self._tracked_options: dict[str, _TrackedOption] = {} self._wrapped_command = _wrapped_command if typing.TYPE_CHECKING: __call__: abc.CommandCallbackSigT else: async def __call__(self, *args, **kwargs) -> None: await self._callback.callback(*args, **kwargs) @property def callback(self) -> abc.CommandCallbackSigT: # <<inherited docstring from tanjun.abc.SlashCommand>>. return typing.cast(abc.CommandCallbackSigT, self._callback.callback) @property def needs_injector(self) -> bool: return ( self._callback.needs_injector or any(option.needs_injector for option in self._tracked_options.values()) or super().needs_injector ) def bind_client(self: _SlashCommandT, client: abc.Client, /) -> _SlashCommandT: self._client = client super().bind_client(client) for option in self._tracked_options.values(): option.check_client(client) return self def build(self) -> special_endpoints_api.CommandBuilder: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. return self._builder.sort().copy() def load_into_component(self, component: abc.Component, /) -> None: super().load_into_component(component) if self._wrapped_command and isinstance(self._wrapped_command, components.AbstractComponentLoader): self._wrapped_command.load_into_component(component) def _add_option( self: _SlashCommandT, name: str, description: str, type_: typing.Union[hikari.OptionType, int] = hikari.OptionType.STRING, /, *, always_float: bool = False, channel_types: typing.Optional[collections.Sequence[int]] = None, choices: typing.Union[ collections.Mapping[str, typing.Union[str, int, float]], collections.Sequence[typing.Any], None ] = None, converters: typing.Union[collections.Iterable[ConverterSig], ConverterSig] = (), default: typing.Any = _UNDEFINED_DEFAULT, min_value: typing.Union[int, float, None] = None, max_value: typing.Union[int, float, None] = None, only_member: bool = False, pass_as_kwarg: bool = True, _stack_level: int = 0, ) -> _SlashCommandT: _validate_name(name) if len(description) > 100: raise ValueError("The option description cannot be over 100 characters in length") if len(self._builder.options) == 25: raise ValueError("Slash commands cannot have more than 25 options") if min_value and max_value and min_value > max_value: raise ValueError("The min value cannot be greater than the max value") type_ = hikari.OptionType(type_) if isinstance(converters, collections.Iterable): converters_ = list(map(_convert_to_injectable, converters)) else: converters_ = [_convert_to_injectable(converters)] if self._client: for converter in converters_: if isinstance(converter.callback, conversion.BaseConverter): converter.callback.check_client(self._client, f"{self._name}'s slash option '{name}'") if choices is None: actual_choices: typing.Optional[list[hikari.CommandChoice]] = None elif isinstance(choices, collections.Mapping): actual_choices = [hikari.CommandChoice(name=name, value=value) for name, value in choices.items()] else: warnings.warn( "Passing a sequence of tuples to `choices` is deprecated since 2.1.2a1, " "please pass a mapping instead.", category=DeprecationWarning, stacklevel=2 + _stack_level, ) actual_choices = [hikari.CommandChoice(name=name, value=value) for name, value in choices] if actual_choices and len(actual_choices) > 25: raise ValueError("Slash command options cannot have more than 25 choices") required = default is _UNDEFINED_DEFAULT self._builder.add_option( hikari.CommandOption( type=type_, name=name, description=description, is_required=required, choices=actual_choices, channel_types=channel_types, min_value=min_value, max_value=max_value, ) ) if pass_as_kwarg: self._tracked_options[name] = _TrackedOption( name=name, option_type=type_, always_float=always_float, converters=converters_, default=default, only_member=only_member, ) return self def add_str_option( self: _SlashCommandT, name: str, description: str, /, *, choices: typing.Union[collections.Mapping[str, str], collections.Sequence[str], None] = None, converters: typing.Union[collections.Sequence[ConverterSig], ConverterSig] = (), default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, _stack_level: int = 0, ) -> _SlashCommandT: r"""Add a string option to the slash command. .. note:: As a shorthand, `choices` also supports passing a list of strings rather than a dict of names to values (each string will used as both the choice's name and value with the names being capitalised). Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- choices : typing.Union[collections.abc.Mapping[str, str], collections.abc.Sequence[str], None] The option's choices. This either a mapping of [option_name, option_value] where both option_name and option_value should be strings of up to 100 characters or a sequence of strings where the string will be used for both the choice's name and value. converters : typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig] The option's converters. This may be either one or multiple `ConverterSig` callbacks used to convert the option's value to the final form. If no converters are provided then the raw value will be passed. Only the first converter to pass will be used. default : typing.Any The option's default value. If this is left as undefined then this option will be required. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used and the `coverters` field will be ignored. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the option has more than 25 choices. * If the command already has 25 options. """ if choices is None: actual_choices = None elif isinstance(choices, collections.Mapping): actual_choices = choices else: actual_choices = {} warned = False for choice in choices: if isinstance(choice, tuple): # type: ignore[unreachable] # the point of this is for deprecation if not warned: # type: ignore[unreachable] # mypy sees `warned = True` and messes up. warnings.warn( "Passing a sequence of tuples for 'choices' is deprecated since 2.1.2a1, " "please pass a mapping instead.", category=DeprecationWarning, stacklevel=2 + _stack_level, ) warned = True actual_choices[choice[0]] = choice[1] else: actual_choices[choice.capitalize()] = choice return self._add_option( name, description, hikari.OptionType.STRING, choices=actual_choices, converters=converters, default=default, pass_as_kwarg=pass_as_kwarg, ) def add_int_option( self: _SlashCommandT, name: str, description: str, /, *, choices: typing.Optional[collections.Mapping[str, int]] = None, converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (), default: typing.Any = _UNDEFINED_DEFAULT, min_value: typing.Optional[int] = None, max_value: typing.Optional[int] = None, pass_as_kwarg: bool = True, _stack_level: int = 0, ) -> _SlashCommandT: r"""Add an integer option to the slash command. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- choices : typing.Optional[collections.abc.Mapping[str, int]] The option's choices. This is a mapping of [option_name, option_value] where option_name should be a string of up to 100 characters and option_value should be an integer. converters : typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig, None] The option's converters. This may be either one or multiple `ConverterSig` callbacks used to convert the option's value to the final form. If no converters are provided then the raw value will be passed. Only the first converter to pass will be used. default : typing.Any The option's default value. If this is left as undefined then this option will be required. min_value : typing.Optional[int] The option's (inclusive) minimum value. Defaults to no minimum value. max_value : typing.Optional[int] The option's (inclusive) maximum value. Defaults to no minimum value. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used and the `coverters` field will be ignored. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the option has more than 25 choices. * If the command already has 25 options. * If `min_value` is greater than `max_value`. """ return self._add_option( name, description, hikari.OptionType.INTEGER, choices=choices, converters=converters, default=default, min_value=min_value, max_value=max_value, pass_as_kwarg=pass_as_kwarg, _stack_level=_stack_level + 1, ) def add_float_option( self: _SlashCommandT, name: str, description: str, /, *, always_float: bool = True, choices: typing.Optional[collections.Mapping[str, float]] = None, converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (), default: typing.Any = _UNDEFINED_DEFAULT, min_value: typing.Optional[float] = None, max_value: typing.Optional[float] = None, pass_as_kwarg: bool = True, _stack_level: int = 0, ) -> _SlashCommandT: r"""Add a float option to a slash command. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- always_float : bool If this is set to `True` then the value will always be converted to a float (this will happen before it's passed to converters). This masks behaviour from Discord where we will either be provided a `float` or `int` dependent on what the user provided and defaults to `True`. choices : typing.Optional[collections.abc.Mapping[str, float]] The option's choices. This is a mapping of [option_name, option_value] where option_name should be a string of up to 100 characters and option_value should be a float. converters : typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig, None] The option's converters. This may be either one or multiple `ConverterSig` callbacks used to convert the option's value to the final form. If no converters are provided then the raw value will be passed. Only the first converter to pass will be used. default : typing.Any The option's default value. If this is left as undefined then this option will be required. min_value : typing.Optional[float] The option's (inclusive) minimum value. Defaults to no minimum value. max_value : typing.Optional[float] The option's (inclusive) maximum value. Defaults to no minimum value. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used and the fields `coverters`, and `always_float` will be ignored. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the option has more than 25 choices. * If the command already has 25 options. * If `min_value` is greater than `max_value`. """ return self._add_option( name, description, hikari.OptionType.FLOAT, choices=choices, converters=converters, default=default, min_value=float(min_value) if min_value is not None else None, max_value=float(max_value) if max_value is not None else None, pass_as_kwarg=pass_as_kwarg, always_float=always_float, _stack_level=_stack_level + 1, ) def add_bool_option( self: _SlashCommandT, name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, ) -> _SlashCommandT: r"""Add a boolean option to a slash command. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- default : typing.Any The option's default value. If this is left as undefined then this option will be required. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the command already has 25 options. """ return self._add_option( name, description, hikari.OptionType.BOOLEAN, default=default, pass_as_kwarg=pass_as_kwarg ) def add_user_option( self: _SlashCommandT, name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, ) -> _SlashCommandT: r"""Add a user option to a slash command. .. note:: This may result in `hikari.InteractionMember` or `hikari.users.User` if the user isn't in the current guild or if this command was executed in a DM channel. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- default : typing.Any The option's default value. If this is left as undefined then this option will be required. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the option has more than 25 choices. * If the command already has 25 options. """ return self._add_option(name, description, hikari.OptionType.USER, default=default, pass_as_kwarg=pass_as_kwarg) def add_member_option( self: _SlashCommandT, name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, ) -> _SlashCommandT: r"""Add a member option to a slash command. .. note:: This will always result in `hikari.InteractionMember`. .. warning:: Unlike the other options, this is an artificial option which adds a restraint to the USER option type and therefore cannot have `pass_as_kwarg` set to `False` as this artificial constaint isn't present when its not being passed as a keyword argument. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- default : typing.Any The option's default value. If this is left as undefined then this option will be required. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the command already has 25 options. """ return self._add_option(name, description, hikari.OptionType.USER, default=default, only_member=True) def add_channel_option( self: _SlashCommandT, name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, types: typing.Optional[collections.Collection[type[hikari.PartialChannel]]] = None, pass_as_kwarg: bool = True, ) -> _SlashCommandT: r"""Add a channel option to a slash command. .. note:: This will always result in `hikari.InteractionChannel`. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Parameters ---------- default : typing.Any The option's default value. If this is left as undefined then this option will be required. types : typing.Optional[collections.abc.Collection[type[hikari.PartialChannel]]] A collection of the channel classes this option should accept. If left as `None` or empty then the option will allow all channel types. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the command already has 25 options. * If an invalid type is passed in `types`. """ import itertools if types: try: channel_types = list(set(itertools.chain.from_iterable(map(_channel_types.__getitem__, types)))) except KeyError as exc: raise ValueError(f"Unknown channel type {exc.args[0]}") from exc else: channel_types = None return self._add_option( name, description, hikari.OptionType.CHANNEL, channel_types=channel_types, default=default, pass_as_kwarg=pass_as_kwarg, ) def add_role_option( self: _SlashCommandT, name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, ) -> _SlashCommandT: r"""Add a role option to a slash command. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- default : typing.Any The option's default value. If this is left as undefined then this option will be required. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the command already has 25 options. """ return self._add_option(name, description, hikari.OptionType.ROLE, default=default, pass_as_kwarg=pass_as_kwarg) def add_mentionable_option( self: _SlashCommandT, name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, ) -> _SlashCommandT: r"""Add a mentionable option to a slash command. .. note:: This may target roles, guild members or users and results in `Union[hikari.User, hikari.InteractionMember, hikari.Role]`. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- default : typing.Any The option's default value. If this is left as undefined then this option will be required. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the command already has 25 options. """ return self._add_option( name, description, hikari.OptionType.MENTIONABLE, default=default, pass_as_kwarg=pass_as_kwarg ) async def _process_args(self, ctx: abc.SlashContext, /) -> collections.Mapping[str, typing.Any]: keyword_args: dict[str, typing.Union[int, float, str, hikari.User, hikari.Role, hikari.InteractionChannel]] = {} for tracked_option in self._tracked_options.values(): if not (option := ctx.options.get(tracked_option.name)): if tracked_option.default is _UNDEFINED_DEFAULT: raise RuntimeError( # TODO: ConversionError? f"Required option {tracked_option.name} is missing data, are you sure your commands" " are up to date?" ) else: keyword_args[tracked_option.name] = tracked_option.default elif option.type is hikari.OptionType.USER: member: typing.Optional[hikari.InteractionMember] = None if tracked_option.is_only_member and not (member := option.resolve_to_member(default=None)): raise errors.ConversionError( f"Couldn't find member for provided user: {option.value}", tracked_option.name ) keyword_args[option.name] = member or option.resolve_to_user() elif option.type is hikari.OptionType.CHANNEL: keyword_args[option.name] = option.resolve_to_channel() elif option.type is hikari.OptionType.ROLE: keyword_args[option.name] = option.resolve_to_role() elif option.type is hikari.OptionType.MENTIONABLE: keyword_args[option.name] = option.resolve_to_mentionable() else: value = option.value # To be type safe we obfuscate the fact that discord's double type will provide an int or float # depending on the value Discord inputs by always casting to float. if tracked_option.type is hikari.OptionType.FLOAT and tracked_option.is_always_float: value = float(value) if tracked_option.converters: value = await tracked_option.convert(ctx, option.value) keyword_args[option.name] = value return keyword_args async def execute( self, ctx: abc.SlashContext, /, option: typing.Optional[hikari.CommandInteractionOption] = None, *, hooks: typing.Optional[collections.MutableSet[abc.SlashHooks]] = None, ) -> None: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. if self._always_defer and not ctx.has_been_deferred and not ctx.has_responded: await ctx.defer() ctx = ctx.set_command(self) own_hooks = self._hooks or _EMPTY_HOOKS try: await own_hooks.trigger_pre_execution(ctx, hooks=hooks) if self._tracked_options: kwargs = await self._process_args(ctx) else: kwargs = _EMPTY_DICT await self._callback.resolve_with_command_context(ctx, ctx, **kwargs) except errors.CommandError as exc: await ctx.respond(exc.message) except errors.HaltExecution: # Unlike a message command, this won't necessarily reach the client level try except # block so we have to handle this here. await ctx.mark_not_found() except Exception as exc: if await own_hooks.trigger_error(ctx, exc, hooks=hooks) <= 0: raise else: await own_hooks.trigger_success(ctx, hooks=hooks) finally: await own_hooks.trigger_post_execution(ctx, hooks=hooks) def copy( self: _SlashCommandT, *, _new: bool = True, parent: typing.Optional[abc.SlashCommandGroup] = None ) -> _SlashCommandT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. if not _new: self._callback = copy.copy(self._callback) return super().copy(_new=_new, parent=parent) return super().copy(_new=_new, parent=parent) def as_message_command( name: str, /, *names: str ) -> collections.Callable[[_CallbackishT[abc.CommandCallbackSigT],], MessageCommand[abc.CommandCallbackSigT]]: """Build a message command from a decorated callback. Parameters ---------- name : str The command name. Other Parameters ---------------- *names : str Variable positional arguments of other names for the command. Returns ------- collections.abc.Callable[[_CallbackishT[CommandCallbackSigT]], MessageCommand[CommandCallbackSigT]] The decorator callback used to make a `MessageCommand`. This can either wrap a raw command callback or another callable command instance (e.g. `SlashCommand`, `MessageCommand`, `MessageCommandGroup`) and will manage loading the other command into a component when using `tanjun.Component.load_from_scope`. """ def decorator( callback: _CallbackishT[abc.CommandCallbackSigT], /, ) -> MessageCommand[abc.CommandCallbackSigT]: if isinstance(callback, (abc.SlashCommand, abc.MessageCommand)): return MessageCommand(callback.callback, name, *names, _wrapped_command=callback) return MessageCommand(callback, name, *names) return decorator def as_message_command_group( name: str, /, *names: str, strict: bool = False ) -> collections.Callable[[_CallbackishT[abc.CommandCallbackSigT]], MessageCommandGroup[abc.CommandCallbackSigT]]: """Build a message command group from a decorated callback. Parameters ---------- name : str The command name. Other Parameters ---------------- *names : str Variable positional arguments of other names for the command. strict : bool Whether this command group should only allow commands without spaces in their names. This allows for a more optimised command search pattern to be used and enforces that command names are unique to a single command within the group. Returns ------- collections.abc.Callable[[_CallbackishT[CommandCallbackSigT]], MessageCommand[CommandCallbackSigT]] The decorator callback used to make a `MessageCommandGroup`. This can either wrap a raw command callback or another callable command instance (e.g. `SlashCommand`, `MessageCommand`, `MessageCommandGroup`) and will manage loading the other command into a component when using `tanjun.Component.load_from_scope`. """ def decorator(callback: _CallbackishT[abc.CommandCallbackSigT], /) -> MessageCommandGroup[abc.CommandCallbackSigT]: if isinstance(callback, (abc.SlashCommand, abc.MessageCommand)): return MessageCommandGroup(callback.callback, name, *names, strict=strict, _wrapped_command=callback) return MessageCommandGroup(callback, name, *names, strict=strict) return decorator class MessageCommand(PartialCommand[abc.MessageContext], abc.MessageCommand[abc.CommandCallbackSigT]): """Standard implementation of a message command.""" __slots__ = ("_callback", "_names", "_parent", "_parser", "_wrapped_command") def __init__( self, callback: abc.CommandCallbackSigT, name: str, /, *names: str, _wrapped_command: typing.Optional[abc.ExecutableCommand[typing.Any]] = None, ) -> None: """Initialise a message command. Parameters ---------- callback : collections.abc.Callable[[tanjun.abc.MessageContext, ...], collections.abc.Awaitable[None]] Callback to execute when the command is invoked. This should be an asynchronous callback which takes one positional argument of type `tanjun.abc.MessageContext`, returns `None` and may use dependency injection to access other services. name : str The command name. Other Parameters ---------------- *names : str Variable positional arguments of other names for the command. """ super().__init__() if not _wrapped_command and isinstance(callback, (abc.MessageCommand, abc.SlashCommand)): callback = typing.cast(abc.CommandCallbackSigT, callback.callback) self._callback = injecting.CallbackDescriptor[None](callback) self._names = list(dict.fromkeys((name, *names))) self._parent: typing.Optional[abc.MessageCommandGroup[typing.Any]] = None self._parser: typing.Optional[abc.MessageParser] = None self._wrapped_command = _wrapped_command def __repr__(self) -> str: return f"Command <{self._names}>" if typing.TYPE_CHECKING: __call__: abc.CommandCallbackSigT else: async def __call__(self, *args, **kwargs) -> None: await self._callback.callback(*args, **kwargs) @property def callback(self) -> abc.CommandCallbackSigT: # <<inherited docstring from tanjun.abc.MessageCommand>>. return typing.cast(abc.CommandCallbackSigT, self._callback.callback) @property # <<inherited docstring from tanjun.abc.MessageCommand>>. def names(self) -> collections.Collection[str]: return self._names.copy() @property def needs_injector(self) -> bool: return self._callback.needs_injector @property def parent(self) -> typing.Optional[abc.MessageCommandGroup[typing.Any]]: # <<inherited docstring from tanjun.abc.MessageCommand>>. return self._parent @property def parser(self) -> typing.Optional[abc.MessageParser]: # <<inherited docstring from tanjun.abc.MessageCommand>>. return self._parser def bind_client(self: _MessageCommandT, client: abc.Client, /) -> _MessageCommandT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. super().bind_client(client) if self._parser: self._parser.bind_client(client) return self def bind_component(self: _MessageCommandT, component: abc.Component, /) -> _MessageCommandT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. super().bind_component(component) if self._parser: self._parser.bind_component(component) return self def copy( self: _MessageCommandT, *, parent: typing.Optional[abc.MessageCommandGroup[typing.Any]] = None, _new: bool = True, ) -> _MessageCommandT: # <<inherited docstring from tanjun.abc.MessageCommand>>. if not _new: self._callback = copy.copy(self._callback) self._names = self._names.copy() self._parent = parent self._parser = self._parser.copy() if self._parser else None return super().copy(_new=_new) return super().copy(_new=_new) def set_parent( self: _MessageCommandT, parent: typing.Optional[abc.MessageCommandGroup[typing.Any]], / ) -> _MessageCommandT: # <<inherited docstring from tanjun.abc.MessageCommand>>. self._parent = parent return self def set_parser(self: _MessageCommandT, parser: typing.Optional[abc.MessageParser], /) -> _MessageCommandT: # <<inherited docstring from tanjun.abc.MessageCommand>>. self._parser = parser return self async def check_context(self, ctx: abc.MessageContext, /) -> bool: # <<inherited docstring from tanjun.abc.MessageCommand>>. ctx.set_command(self) result = await utilities.gather_checks(ctx, self._checks) ctx.set_command(None) return result async def execute( self, ctx: abc.MessageContext, /, *, hooks: typing.Optional[collections.MutableSet[abc.MessageHooks]] = None, ) -> None: # <<inherited docstring from tanjun.abc.MessageCommand>>. ctx = ctx.set_command(self) own_hooks = self._hooks or _EMPTY_HOOKS try: await own_hooks.trigger_pre_execution(ctx, hooks=hooks) if self._parser is not None: kwargs = await self._parser.parse(ctx) else: kwargs = _EMPTY_DICT await self._callback.resolve_with_command_context(ctx, ctx, **kwargs) except errors.CommandError as exc: response = exc.message if len(exc.message) <= 2000 else exc.message[:1997] + "..." await ctx.respond(content=response) except errors.HaltExecution: raise except Exception as exc: if await own_hooks.trigger_error(ctx, exc, hooks=hooks) <= 0: raise else: # TODO: how should this be handled around CommandError? await own_hooks.trigger_success(ctx, hooks=hooks) finally: await own_hooks.trigger_post_execution(ctx, hooks=hooks) def load_into_component(self, component: abc.Component, /) -> None: # <<inherited docstring from tanjun.components.load_into_component>>. if not self._parent: component.add_message_command(self) if self._wrapped_command and isinstance(self._wrapped_command, components.AbstractComponentLoader): self._wrapped_command.load_into_component(component) class MessageCommandGroup(MessageCommand[abc.CommandCallbackSigT], abc.MessageCommandGroup[abc.CommandCallbackSigT]): """Standard implementation of a message command group.""" __slots__ = ("_commands", "_is_strict", "_names_to_commands") def __init__( self, callback: abc.CommandCallbackSigT, name: str, /, *names: str, strict: bool = False, _wrapped_command: typing.Optional[abc.ExecutableCommand[typing.Any]] = None, ) -> None: """Initialise a message command group. Parameters ---------- name : str The command name. Other Parameters ---------------- *names : str Variable positional arguments of other names for the command. strict : bool Whether this command group should only allow commands without spaces in their names. This allows for a more optimised command search pattern to be used and enforces that command names are unique to a single command within the group. """ super().__init__(callback, name, *names, _wrapped_command=_wrapped_command) self._commands: list[abc.MessageCommand[typing.Any]] = [] self._is_strict = strict self._names_to_commands: dict[str, abc.MessageCommand[typing.Any]] = {} def __repr__(self) -> str: return f"CommandGroup <{len(self._commands)}: {self._names}>" @property def commands(self) -> collections.Collection[abc.MessageCommand[typing.Any]]: # <<inherited docstring from tanjun.abc.MessageCommandGroup>>. return self._commands.copy() @property def is_strict(self) -> bool: return self._is_strict def copy( self: _MessageCommandGroupT, *, parent: typing.Optional[abc.MessageCommandGroup[typing.Any]] = None, _new: bool = True, ) -> _MessageCommandGroupT: # <<inherited docstring from tanjun.abc.MessageCommand>>. if not _new: commands = {command: command.copy(parent=self) for command in self._commands} self._commands = list(commands.values()) self._names_to_commands = {name: commands[command] for name, command in self._names_to_commands.items()} return super().copy(parent=parent, _new=_new) return super().copy(parent=parent, _new=_new) def add_command(self: _MessageCommandGroupT, command: abc.MessageCommand[typing.Any], /) -> _MessageCommandGroupT: """Add a command to this group. Parameters ---------- command : MessageCommand The command to add. Returns ------- Self The group instance to enable chained calls. Raises ------ ValueError If one of the command's names is already registered in a strict command group. """ if command in self._commands: return self if self._is_strict: if any(" " in name for name in command.names): raise ValueError("Sub-command names may not contain spaces in a strict message command group") if name_conflicts := self._names_to_commands.keys() & command.names: raise ValueError( "Sub-command names must be unique in a strict message command group. " "The following conflicts were found " + ", ".join(name_conflicts) ) self._names_to_commands.update((name, command) for name in command.names) command.set_parent(self) self._commands.append(command) return self def remove_command( self: _MessageCommandGroupT, command: abc.MessageCommand[typing.Any], / ) -> _MessageCommandGroupT: # <<inherited docstring from tanjun.abc.MessageCommandGroup>>. self._commands.remove(command) if self._is_strict: for name in command.names: if self._names_to_commands.get(name) == command: del self._names_to_commands[name] command.set_parent(None) return self def with_command(self, command: AnyMessageCommandT, /) -> AnyMessageCommandT: self.add_command(command) return command def bind_client(self: _MessageCommandGroupT, client: abc.Client, /) -> _MessageCommandGroupT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. super().bind_client(client) for command in self._commands: command.bind_client(client) return self def bind_component(self: _MessageCommandGroupT, component: abc.Component, /) -> _MessageCommandGroupT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. super().bind_component(component) for command in self._commands: command.bind_component(component) return self def find_command(self, content: str, /) -> collections.Iterable[tuple[str, abc.MessageCommand[typing.Any]]]: if self._is_strict: name = content.split(" ")[0] if command := self._names_to_commands.get(name): yield name, command return for command in self._commands: if (name_ := utilities.match_prefix_names(content, command.names)) is not None: yield name_, command async def execute( self, ctx: abc.MessageContext, /, *, hooks: typing.Optional[collections.MutableSet[abc.MessageHooks]] = None, ) -> None: # <<inherited docstring from tanjun.abc.MessageCommand>>. if ctx.message.content is None: raise ValueError("Cannot execute a command with a content-less message") if self._hooks: if hooks is None: hooks = set() hooks.add(self._hooks) for name, command in self.find_command(ctx.content): if await command.check_context(ctx): content = ctx.content[len(name) :] lstripped_content = content.lstrip() space_len = len(content) - len(lstripped_content) ctx.set_triggering_name(ctx.triggering_name + (" " * space_len) + name) ctx.set_content(lstripped_content) await command.execute(ctx, hooks=hooks) return await super().execute(ctx, hooks=hooks)
Standard implementation of Tanjun's command objects.
View Source
def as_message_command( name: str, /, *names: str ) -> collections.Callable[[_CallbackishT[abc.CommandCallbackSigT],], MessageCommand[abc.CommandCallbackSigT]]: """Build a message command from a decorated callback. Parameters ---------- name : str The command name. Other Parameters ---------------- *names : str Variable positional arguments of other names for the command. Returns ------- collections.abc.Callable[[_CallbackishT[CommandCallbackSigT]], MessageCommand[CommandCallbackSigT]] The decorator callback used to make a `MessageCommand`. This can either wrap a raw command callback or another callable command instance (e.g. `SlashCommand`, `MessageCommand`, `MessageCommandGroup`) and will manage loading the other command into a component when using `tanjun.Component.load_from_scope`. """ def decorator( callback: _CallbackishT[abc.CommandCallbackSigT], /, ) -> MessageCommand[abc.CommandCallbackSigT]: if isinstance(callback, (abc.SlashCommand, abc.MessageCommand)): return MessageCommand(callback.callback, name, *names, _wrapped_command=callback) return MessageCommand(callback, name, *names) return decorator
Build a message command from a decorated callback.
Parameters
- name (str): The command name.
Other Parameters
- *names (str): Variable positional arguments of other names for the command.
Returns
- collections.abc.Callable[[_CallbackishT[CommandCallbackSigT]], MessageCommand[CommandCallbackSigT]]: The decorator callback used to make a
MessageCommand.
This can either wrap a raw command callback or another callable command instance
(e.g. SlashCommand, MessageCommand, MessageCommandGroup) and will manage
loading the other command into a component when using tanjun.Component.load_from_scope.
View Source
def as_message_command_group( name: str, /, *names: str, strict: bool = False ) -> collections.Callable[[_CallbackishT[abc.CommandCallbackSigT]], MessageCommandGroup[abc.CommandCallbackSigT]]: """Build a message command group from a decorated callback. Parameters ---------- name : str The command name. Other Parameters ---------------- *names : str Variable positional arguments of other names for the command. strict : bool Whether this command group should only allow commands without spaces in their names. This allows for a more optimised command search pattern to be used and enforces that command names are unique to a single command within the group. Returns ------- collections.abc.Callable[[_CallbackishT[CommandCallbackSigT]], MessageCommand[CommandCallbackSigT]] The decorator callback used to make a `MessageCommandGroup`. This can either wrap a raw command callback or another callable command instance (e.g. `SlashCommand`, `MessageCommand`, `MessageCommandGroup`) and will manage loading the other command into a component when using `tanjun.Component.load_from_scope`. """ def decorator(callback: _CallbackishT[abc.CommandCallbackSigT], /) -> MessageCommandGroup[abc.CommandCallbackSigT]: if isinstance(callback, (abc.SlashCommand, abc.MessageCommand)): return MessageCommandGroup(callback.callback, name, *names, strict=strict, _wrapped_command=callback) return MessageCommandGroup(callback, name, *names, strict=strict) return decorator
Build a message command group from a decorated callback.
Parameters
- name (str): The command name.
Other Parameters
- *names (str): Variable positional arguments of other names for the command.
strict (bool): Whether this command group should only allow commands without spaces in their names.
This allows for a more optimised command search pattern to be used and enforces that command names are unique to a single command within the group.
Returns
- collections.abc.Callable[[_CallbackishT[CommandCallbackSigT]], MessageCommand[CommandCallbackSigT]]: The decorator callback used to make a
MessageCommandGroup.
This can either wrap a raw command callback or another callable command instance
(e.g. SlashCommand, MessageCommand, MessageCommandGroup) and will manage
loading the other command into a component when using tanjun.Component.load_from_scope.
View Source
def as_slash_command( name: str, description: str, /, *, always_defer: bool = False, default_permission: bool = True, default_to_ephemeral: typing.Optional[bool] = None, is_global: bool = True, sort_options: bool = True, ) -> collections.Callable[[_CallbackishT[abc.CommandCallbackSigT],], SlashCommand[abc.CommandCallbackSigT]]: r"""Build a `SlashCommand` by decorating a function. .. note:: Under the standard implementation, `is_global` is used to determine whether the command should be bulk set by `tanjun.Client.set_global_commands` or when `set_global_commands` is True .. warning:: `default_permission` and `is_global` are ignored for commands within slash command groups. Examples -------- ```py @as_slash_command("ping", "Get the bot's latency") async def ping_command(self, ctx: tanjun.abc.SlashContext) -> None: start_time = time.perf_counter() await ctx.rest.fetch_my_user() time_taken = (time.perf_counter() - start_time) * 1_000 await ctx.respond(f"PONG\n - REST: {time_taken:.0f}mss") ``` Parameters ---------- name : str The command's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The command's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- always_defer : bool Whether the contexts this command is executed with should always be deferred before being passed to the command's callback. Defaults to `False`. .. note:: The ephemeral state of the first response is decided by whether the deferral is ephemeral. default_permission : bool Whether this command can be accessed without set permissions. Defaults to `True`, meaning that users can access the command by default. default_to_ephemeral : typing.Optional[bool] Whether this command's responses should default to ephemeral unless flags are set to override this. If this is left as `None` then the default set on the parent command(s), component or client will be in effect. is_global : bool Whether this command is a global command. Defaults to `True`. sort_options : bool Whether this command should sort its set options based on whether they're required. If this is `True` then the options are re-sorted to meet the requirement from Discord that required command options be listed before optional ones. Returns ------- collections.abc.Callable[[_CallbackishT[CommandCallbackSigT]], SlashCommand[CommandCallbackSigT]] The decorator callback used to make a `SlashCommand`. This can either wrap a raw command callback or another callable command instance (e.g. `SlashCommand`, `MessageCommand`, `MessageCommandGroup`) and will manage loading the other command into a component when using `tanjun.Component.load_from_scope`. Raises ------ ValueError Raises a value error for any of the following reasons: * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the command name has uppercase characters. * If the description is over 100 characters long. """ def decorator(callback: _CallbackishT[abc.CommandCallbackSigT], /) -> SlashCommand[abc.CommandCallbackSigT]: if isinstance(callback, (abc.SlashCommand, abc.MessageCommand)): return SlashCommand( callback.callback, name, description, always_defer=always_defer, default_permission=default_permission, default_to_ephemeral=default_to_ephemeral, is_global=is_global, sort_options=sort_options, _wrapped_command=callback, ) return SlashCommand( callback, name, description, always_defer=always_defer, default_permission=default_permission, default_to_ephemeral=default_to_ephemeral, is_global=is_global, sort_options=sort_options, ) return decorator
Build a SlashCommand by decorating a function.
Note:
Under the standard implementation, is_global is used to determine whether
the command should be bulk set by tanjun.Client.set_global_commands
or when set_global_commands is True
Warning:
default_permission and is_global are ignored for commands within
slash command groups.
Examples
@as_slash_command("ping", "Get the bot's latency")
async def ping_command(self, ctx: tanjun.abc.SlashContext) -> None:
start_time = time.perf_counter()
await ctx.rest.fetch_my_user()
time_taken = (time.perf_counter() - start_time) * 1_000
await ctx.respond(f"PONG\n - REST: {time_taken:.0f}mss")
Parameters
name (str): The command's name.
This must match the regex
^[\w-]{1,32}in Unicode mode and be lowercase.- description (str): The command's description. This should be inclusively between 1-100 characters in length.
Other Parameters
always_defer (bool): Whether the contexts this command is executed with should always be deferred before being passed to the command's callback.
Defaults to
False.Note: The ephemeral state of the first response is decided by whether the deferral is ephemeral.
default_permission (bool): Whether this command can be accessed without set permissions.
Defaults to
True, meaning that users can access the command by default.default_to_ephemeral (typing.Optional[bool]): Whether this command's responses should default to ephemeral unless flags are set to override this.
If this is left as
Nonethen the default set on the parent command(s), component or client will be in effect.- is_global (bool):
Whether this command is a global command. Defaults to
True. sort_options (bool): Whether this command should sort its set options based on whether they're required.
If this is
Truethen the options are re-sorted to meet the requirement from Discord that required command options be listed before optional ones.
Returns
- collections.abc.Callable[[_CallbackishT[CommandCallbackSigT]], SlashCommand[CommandCallbackSigT]]: The decorator callback used to make a
SlashCommand.
This can either wrap a raw command callback or another callable command instance
(e.g. SlashCommand, MessageCommand, MessageCommandGroup) and will manage
loading the other command into a component when using tanjun.Component.load_from_scope.
Raises
- ValueError: Raises a value error for any of the following reasons:
- If the command name doesn't match the regex
^[\w-]{1,32}$(Unicode mode). - If the command name has uppercase characters.
- If the description is over 100 characters long.
- If the command name doesn't match the regex
View Source
def slash_command_group( name: str, description: str, /, *, default_permission: bool = True, default_to_ephemeral: typing.Optional[bool] = None, is_global: bool = True, ) -> SlashCommandGroup: r"""Create a slash command group. Examples -------- Sub-commands can be added to the created slash command object through the following decorator based approach: ```python help_group = tanjun.slash_command_group("help", "get help") @help_group.with_command @tanjun.with_str_slash_option("command_name", "command name") @tanjun.as_slash_command("command", "Get help with a command") async def help_command_command(ctx: tanjun.abc.SlashContext, command_name: str) -> None: ... @help_group.with_command @tanjun.as_slash_command("me", "help me") async def help_me_command(ctx: tanjun.abc.SlashContext) -> None: ... component = tanjun.Component().add_slash_command(help_group) ``` Notes ----- * Unlike message command grups, slash command groups cannot be callable functions themselves. * Under the standard implementation, `is_global` is used to determine whether the command should be bulk set by `tanjun.Client.set_global_commands` or when `set_global_commands` is True Parameters ---------- name : str The name of the command group. This must match the regex `^[\w-]{1,32}$` in Unicode mode and be lowercase. description : str The description of the command group. Other Parameters ---------------- default_permission : bool Whether this command can be accessed without set permissions. Defaults to `True`, meaning that users can access the command by default. default_to_ephemeral : typing.Optional[bool] Whether this command's responses should default to ephemeral unless flags are set to override this. If this is left as `None` then the default set on the parent command(s), component or client will be in effect. is_global : bool Whether this command is a global command. Defaults to `True`. Returns ------- SlashCommandGroup The command group. Raises ------ ValueError Raises a value error for any of the following reasons: * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the command name has uppercase characters. * If the description is over 100 characters long. """ return SlashCommandGroup( name, description, default_permission=default_permission, default_to_ephemeral=default_to_ephemeral, is_global=is_global, )
Create a slash command group.
Examples
Sub-commands can be added to the created slash command object through the following decorator based approach:
help_group = tanjun.slash_command_group("help", "get help")
@help_group.with_command
@tanjun.with_str_slash_option("command_name", "command name")
@tanjun.as_slash_command("command", "Get help with a command")
async def help_command_command(ctx: tanjun.abc.SlashContext, command_name: str) -> None:
...
@help_group.with_command
@tanjun.as_slash_command("me", "help me")
async def help_me_command(ctx: tanjun.abc.SlashContext) -> None:
...
component = tanjun.Component().add_slash_command(help_group)
Notes
- Unlike message command grups, slash command groups cannot be callable functions themselves.
- Under the standard implementation,
is_globalis used to determine whether the command should be bulk set bytanjun.Client.set_global_commandsor whenset_global_commandsis True
Parameters
name (str): The name of the command group.
This must match the regex
^[\w-]{1,32}$in Unicode mode and be lowercase.- description (str): The description of the command group.
Other Parameters
default_permission (bool): Whether this command can be accessed without set permissions.
Defaults to
True, meaning that users can access the command by default.default_to_ephemeral (typing.Optional[bool]): Whether this command's responses should default to ephemeral unless flags are set to override this.
If this is left as
Nonethen the default set on the parent command(s), component or client will be in effect.- is_global (bool):
Whether this command is a global command. Defaults to
True.
Returns
- SlashCommandGroup: The command group.
Raises
- ValueError: Raises a value error for any of the following reasons:
- If the command name doesn't match the regex
^[\w-]{1,32}$(Unicode mode). - If the command name has uppercase characters.
- If the description is over 100 characters long.
- If the command name doesn't match the regex
View Source
class MessageCommand(PartialCommand[abc.MessageContext], abc.MessageCommand[abc.CommandCallbackSigT]): """Standard implementation of a message command.""" __slots__ = ("_callback", "_names", "_parent", "_parser", "_wrapped_command") def __init__( self, callback: abc.CommandCallbackSigT, name: str, /, *names: str, _wrapped_command: typing.Optional[abc.ExecutableCommand[typing.Any]] = None, ) -> None: """Initialise a message command. Parameters ---------- callback : collections.abc.Callable[[tanjun.abc.MessageContext, ...], collections.abc.Awaitable[None]] Callback to execute when the command is invoked. This should be an asynchronous callback which takes one positional argument of type `tanjun.abc.MessageContext`, returns `None` and may use dependency injection to access other services. name : str The command name. Other Parameters ---------------- *names : str Variable positional arguments of other names for the command. """ super().__init__() if not _wrapped_command and isinstance(callback, (abc.MessageCommand, abc.SlashCommand)): callback = typing.cast(abc.CommandCallbackSigT, callback.callback) self._callback = injecting.CallbackDescriptor[None](callback) self._names = list(dict.fromkeys((name, *names))) self._parent: typing.Optional[abc.MessageCommandGroup[typing.Any]] = None self._parser: typing.Optional[abc.MessageParser] = None self._wrapped_command = _wrapped_command def __repr__(self) -> str: return f"Command <{self._names}>" if typing.TYPE_CHECKING: __call__: abc.CommandCallbackSigT else: async def __call__(self, *args, **kwargs) -> None: await self._callback.callback(*args, **kwargs) @property def callback(self) -> abc.CommandCallbackSigT: # <<inherited docstring from tanjun.abc.MessageCommand>>. return typing.cast(abc.CommandCallbackSigT, self._callback.callback) @property # <<inherited docstring from tanjun.abc.MessageCommand>>. def names(self) -> collections.Collection[str]: return self._names.copy() @property def needs_injector(self) -> bool: return self._callback.needs_injector @property def parent(self) -> typing.Optional[abc.MessageCommandGroup[typing.Any]]: # <<inherited docstring from tanjun.abc.MessageCommand>>. return self._parent @property def parser(self) -> typing.Optional[abc.MessageParser]: # <<inherited docstring from tanjun.abc.MessageCommand>>. return self._parser def bind_client(self: _MessageCommandT, client: abc.Client, /) -> _MessageCommandT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. super().bind_client(client) if self._parser: self._parser.bind_client(client) return self def bind_component(self: _MessageCommandT, component: abc.Component, /) -> _MessageCommandT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. super().bind_component(component) if self._parser: self._parser.bind_component(component) return self def copy( self: _MessageCommandT, *, parent: typing.Optional[abc.MessageCommandGroup[typing.Any]] = None, _new: bool = True, ) -> _MessageCommandT: # <<inherited docstring from tanjun.abc.MessageCommand>>. if not _new: self._callback = copy.copy(self._callback) self._names = self._names.copy() self._parent = parent self._parser = self._parser.copy() if self._parser else None return super().copy(_new=_new) return super().copy(_new=_new) def set_parent( self: _MessageCommandT, parent: typing.Optional[abc.MessageCommandGroup[typing.Any]], / ) -> _MessageCommandT: # <<inherited docstring from tanjun.abc.MessageCommand>>. self._parent = parent return self def set_parser(self: _MessageCommandT, parser: typing.Optional[abc.MessageParser], /) -> _MessageCommandT: # <<inherited docstring from tanjun.abc.MessageCommand>>. self._parser = parser return self async def check_context(self, ctx: abc.MessageContext, /) -> bool: # <<inherited docstring from tanjun.abc.MessageCommand>>. ctx.set_command(self) result = await utilities.gather_checks(ctx, self._checks) ctx.set_command(None) return result async def execute( self, ctx: abc.MessageContext, /, *, hooks: typing.Optional[collections.MutableSet[abc.MessageHooks]] = None, ) -> None: # <<inherited docstring from tanjun.abc.MessageCommand>>. ctx = ctx.set_command(self) own_hooks = self._hooks or _EMPTY_HOOKS try: await own_hooks.trigger_pre_execution(ctx, hooks=hooks) if self._parser is not None: kwargs = await self._parser.parse(ctx) else: kwargs = _EMPTY_DICT await self._callback.resolve_with_command_context(ctx, ctx, **kwargs) except errors.CommandError as exc: response = exc.message if len(exc.message) <= 2000 else exc.message[:1997] + "..." await ctx.respond(content=response) except errors.HaltExecution: raise except Exception as exc: if await own_hooks.trigger_error(ctx, exc, hooks=hooks) <= 0: raise else: # TODO: how should this be handled around CommandError? await own_hooks.trigger_success(ctx, hooks=hooks) finally: await own_hooks.trigger_post_execution(ctx, hooks=hooks) def load_into_component(self, component: abc.Component, /) -> None: # <<inherited docstring from tanjun.components.load_into_component>>. if not self._parent: component.add_message_command(self) if self._wrapped_command and isinstance(self._wrapped_command, components.AbstractComponentLoader): self._wrapped_command.load_into_component(component)
Standard implementation of a message command.
View Source
def __init__( self, callback: abc.CommandCallbackSigT, name: str, /, *names: str, _wrapped_command: typing.Optional[abc.ExecutableCommand[typing.Any]] = None, ) -> None: """Initialise a message command. Parameters ---------- callback : collections.abc.Callable[[tanjun.abc.MessageContext, ...], collections.abc.Awaitable[None]] Callback to execute when the command is invoked. This should be an asynchronous callback which takes one positional argument of type `tanjun.abc.MessageContext`, returns `None` and may use dependency injection to access other services. name : str The command name. Other Parameters ---------------- *names : str Variable positional arguments of other names for the command. """ super().__init__() if not _wrapped_command and isinstance(callback, (abc.MessageCommand, abc.SlashCommand)): callback = typing.cast(abc.CommandCallbackSigT, callback.callback) self._callback = injecting.CallbackDescriptor[None](callback) self._names = list(dict.fromkeys((name, *names))) self._parent: typing.Optional[abc.MessageCommandGroup[typing.Any]] = None self._parser: typing.Optional[abc.MessageParser] = None self._wrapped_command = _wrapped_command
Initialise a message command.
Parameters
callback (collections.abc.Callable[[tanjun.abc.MessageContext, ...], collections.abc.Awaitable[None]]): Callback to execute when the command is invoked.
This should be an asynchronous callback which takes one positional argument of type
tanjun.abc.MessageContext, returnsNoneand may use dependency injection to access other services.- name (str): The command name.
Other Parameters
- *names (str): Variable positional arguments of other names for the command.
Callback which is called during execution.
Note: For command groups, this is called when none of the inner-commands matches the message.
Collection of this command's names.
Parent group of this command if applicable.
Parser for this command.
View Source
def bind_client(self: _MessageCommandT, client: abc.Client, /) -> _MessageCommandT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. super().bind_client(client) if self._parser: self._parser.bind_client(client) return self
View Source
def bind_component(self: _MessageCommandT, component: abc.Component, /) -> _MessageCommandT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. super().bind_component(component) if self._parser: self._parser.bind_component(component) return self
View Source
def copy( self: _MessageCommandT, *, parent: typing.Optional[abc.MessageCommandGroup[typing.Any]] = None, _new: bool = True, ) -> _MessageCommandT: # <<inherited docstring from tanjun.abc.MessageCommand>>. if not _new: self._callback = copy.copy(self._callback) self._names = self._names.copy() self._parent = parent self._parser = self._parser.copy() if self._parser else None return super().copy(_new=_new) return super().copy(_new=_new)
Create a copy of this command.
Other Parameters
- parent (typing.Optional[MessageCommandGroup[tping.Any]]): The parent of the copy.
Returns
- Self: The copy.
View Source
def set_parent( self: _MessageCommandT, parent: typing.Optional[abc.MessageCommandGroup[typing.Any]], / ) -> _MessageCommandT: # <<inherited docstring from tanjun.abc.MessageCommand>>. self._parent = parent return self
Set the parent of this command.
Parameters
- parent (typing.Optional[MessageCommandGroup[typing.Any]]): The parent of this command.
Returns
- Self: The command instance to enable chained calls.
View Source
def set_parser(self: _MessageCommandT, parser: typing.Optional[abc.MessageParser], /) -> _MessageCommandT: # <<inherited docstring from tanjun.abc.MessageCommand>>. self._parser = parser return self
Set the for this message command.
Parameters
- parser (MessageParser): The parser to set.
Returns
- Self: The command instance to enable chained calls.
View Source
async def check_context(self, ctx: abc.MessageContext, /) -> bool: # <<inherited docstring from tanjun.abc.MessageCommand>>. ctx.set_command(self) result = await utilities.gather_checks(ctx, self._checks) ctx.set_command(None) return result
View Source
async def execute( self, ctx: abc.MessageContext, /, *, hooks: typing.Optional[collections.MutableSet[abc.MessageHooks]] = None, ) -> None: # <<inherited docstring from tanjun.abc.MessageCommand>>. ctx = ctx.set_command(self) own_hooks = self._hooks or _EMPTY_HOOKS try: await own_hooks.trigger_pre_execution(ctx, hooks=hooks) if self._parser is not None: kwargs = await self._parser.parse(ctx) else: kwargs = _EMPTY_DICT await self._callback.resolve_with_command_context(ctx, ctx, **kwargs) except errors.CommandError as exc: response = exc.message if len(exc.message) <= 2000 else exc.message[:1997] + "..." await ctx.respond(content=response) except errors.HaltExecution: raise except Exception as exc: if await own_hooks.trigger_error(ctx, exc, hooks=hooks) <= 0: raise else: # TODO: how should this be handled around CommandError? await own_hooks.trigger_success(ctx, hooks=hooks) finally: await own_hooks.trigger_post_execution(ctx, hooks=hooks)
View Source
def load_into_component(self, component: abc.Component, /) -> None: # <<inherited docstring from tanjun.components.load_into_component>>. if not self._parent: component.add_message_command(self) if self._wrapped_command and isinstance(self._wrapped_command, components.AbstractComponentLoader): self._wrapped_command.load_into_component(component)
Load the object into the component.
Parameters
- component (tanjun.abc.Component): The component this object should be loaded into.
View Source
class MessageCommandGroup(MessageCommand[abc.CommandCallbackSigT], abc.MessageCommandGroup[abc.CommandCallbackSigT]): """Standard implementation of a message command group.""" __slots__ = ("_commands", "_is_strict", "_names_to_commands") def __init__( self, callback: abc.CommandCallbackSigT, name: str, /, *names: str, strict: bool = False, _wrapped_command: typing.Optional[abc.ExecutableCommand[typing.Any]] = None, ) -> None: """Initialise a message command group. Parameters ---------- name : str The command name. Other Parameters ---------------- *names : str Variable positional arguments of other names for the command. strict : bool Whether this command group should only allow commands without spaces in their names. This allows for a more optimised command search pattern to be used and enforces that command names are unique to a single command within the group. """ super().__init__(callback, name, *names, _wrapped_command=_wrapped_command) self._commands: list[abc.MessageCommand[typing.Any]] = [] self._is_strict = strict self._names_to_commands: dict[str, abc.MessageCommand[typing.Any]] = {} def __repr__(self) -> str: return f"CommandGroup <{len(self._commands)}: {self._names}>" @property def commands(self) -> collections.Collection[abc.MessageCommand[typing.Any]]: # <<inherited docstring from tanjun.abc.MessageCommandGroup>>. return self._commands.copy() @property def is_strict(self) -> bool: return self._is_strict def copy( self: _MessageCommandGroupT, *, parent: typing.Optional[abc.MessageCommandGroup[typing.Any]] = None, _new: bool = True, ) -> _MessageCommandGroupT: # <<inherited docstring from tanjun.abc.MessageCommand>>. if not _new: commands = {command: command.copy(parent=self) for command in self._commands} self._commands = list(commands.values()) self._names_to_commands = {name: commands[command] for name, command in self._names_to_commands.items()} return super().copy(parent=parent, _new=_new) return super().copy(parent=parent, _new=_new) def add_command(self: _MessageCommandGroupT, command: abc.MessageCommand[typing.Any], /) -> _MessageCommandGroupT: """Add a command to this group. Parameters ---------- command : MessageCommand The command to add. Returns ------- Self The group instance to enable chained calls. Raises ------ ValueError If one of the command's names is already registered in a strict command group. """ if command in self._commands: return self if self._is_strict: if any(" " in name for name in command.names): raise ValueError("Sub-command names may not contain spaces in a strict message command group") if name_conflicts := self._names_to_commands.keys() & command.names: raise ValueError( "Sub-command names must be unique in a strict message command group. " "The following conflicts were found " + ", ".join(name_conflicts) ) self._names_to_commands.update((name, command) for name in command.names) command.set_parent(self) self._commands.append(command) return self def remove_command( self: _MessageCommandGroupT, command: abc.MessageCommand[typing.Any], / ) -> _MessageCommandGroupT: # <<inherited docstring from tanjun.abc.MessageCommandGroup>>. self._commands.remove(command) if self._is_strict: for name in command.names: if self._names_to_commands.get(name) == command: del self._names_to_commands[name] command.set_parent(None) return self def with_command(self, command: AnyMessageCommandT, /) -> AnyMessageCommandT: self.add_command(command) return command def bind_client(self: _MessageCommandGroupT, client: abc.Client, /) -> _MessageCommandGroupT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. super().bind_client(client) for command in self._commands: command.bind_client(client) return self def bind_component(self: _MessageCommandGroupT, component: abc.Component, /) -> _MessageCommandGroupT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. super().bind_component(component) for command in self._commands: command.bind_component(component) return self def find_command(self, content: str, /) -> collections.Iterable[tuple[str, abc.MessageCommand[typing.Any]]]: if self._is_strict: name = content.split(" ")[0] if command := self._names_to_commands.get(name): yield name, command return for command in self._commands: if (name_ := utilities.match_prefix_names(content, command.names)) is not None: yield name_, command async def execute( self, ctx: abc.MessageContext, /, *, hooks: typing.Optional[collections.MutableSet[abc.MessageHooks]] = None, ) -> None: # <<inherited docstring from tanjun.abc.MessageCommand>>. if ctx.message.content is None: raise ValueError("Cannot execute a command with a content-less message") if self._hooks: if hooks is None: hooks = set() hooks.add(self._hooks) for name, command in self.find_command(ctx.content): if await command.check_context(ctx): content = ctx.content[len(name) :] lstripped_content = content.lstrip() space_len = len(content) - len(lstripped_content) ctx.set_triggering_name(ctx.triggering_name + (" " * space_len) + name) ctx.set_content(lstripped_content) await command.execute(ctx, hooks=hooks) return await super().execute(ctx, hooks=hooks)
Standard implementation of a message command group.
View Source
def __init__( self, callback: abc.CommandCallbackSigT, name: str, /, *names: str, strict: bool = False, _wrapped_command: typing.Optional[abc.ExecutableCommand[typing.Any]] = None, ) -> None: """Initialise a message command group. Parameters ---------- name : str The command name. Other Parameters ---------------- *names : str Variable positional arguments of other names for the command. strict : bool Whether this command group should only allow commands without spaces in their names. This allows for a more optimised command search pattern to be used and enforces that command names are unique to a single command within the group. """ super().__init__(callback, name, *names, _wrapped_command=_wrapped_command) self._commands: list[abc.MessageCommand[typing.Any]] = [] self._is_strict = strict self._names_to_commands: dict[str, abc.MessageCommand[typing.Any]] = {}
Initialise a message command group.
Parameters
- name (str): The command name.
Other Parameters
- *names (str): Variable positional arguments of other names for the command.
strict (bool): Whether this command group should only allow commands without spaces in their names.
This allows for a more optimised command search pattern to be used and enforces that command names are unique to a single command within the group.
Collection of the commands in this group.
Note: This may include command groups.
View Source
def copy( self: _MessageCommandGroupT, *, parent: typing.Optional[abc.MessageCommandGroup[typing.Any]] = None, _new: bool = True, ) -> _MessageCommandGroupT: # <<inherited docstring from tanjun.abc.MessageCommand>>. if not _new: commands = {command: command.copy(parent=self) for command in self._commands} self._commands = list(commands.values()) self._names_to_commands = {name: commands[command] for name, command in self._names_to_commands.items()} return super().copy(parent=parent, _new=_new) return super().copy(parent=parent, _new=_new)
Create a copy of this command.
Other Parameters
- parent (typing.Optional[MessageCommandGroup[tping.Any]]): The parent of the copy.
Returns
- Self: The copy.
View Source
def add_command(self: _MessageCommandGroupT, command: abc.MessageCommand[typing.Any], /) -> _MessageCommandGroupT: """Add a command to this group. Parameters ---------- command : MessageCommand The command to add. Returns ------- Self The group instance to enable chained calls. Raises ------ ValueError If one of the command's names is already registered in a strict command group. """ if command in self._commands: return self if self._is_strict: if any(" " in name for name in command.names): raise ValueError("Sub-command names may not contain spaces in a strict message command group") if name_conflicts := self._names_to_commands.keys() & command.names: raise ValueError( "Sub-command names must be unique in a strict message command group. " "The following conflicts were found " + ", ".join(name_conflicts) ) self._names_to_commands.update((name, command) for name in command.names) command.set_parent(self) self._commands.append(command) return self
Add a command to this group.
Parameters
- command (MessageCommand): The command to add.
Returns
- Self: The group instance to enable chained calls.
Raises
- ValueError: If one of the command's names is already registered in a strict command group.
View Source
def remove_command( self: _MessageCommandGroupT, command: abc.MessageCommand[typing.Any], / ) -> _MessageCommandGroupT: # <<inherited docstring from tanjun.abc.MessageCommandGroup>>. self._commands.remove(command) if self._is_strict: for name in command.names: if self._names_to_commands.get(name) == command: del self._names_to_commands[name] command.set_parent(None) return self
Remove a command from this group.
Parameters
- command (MessageCommand): The command to remove.
Raises
- ValueError: If the provided command isn't found.
Returns
- Self: The group instance to enable chained calls.
View Source
def with_command(self, command: AnyMessageCommandT, /) -> AnyMessageCommandT: self.add_command(command) return command
Add a command to this group through a decorator call.
Parameters
- command (MessageCommand): The command to add.
Returns
- MessageCommand: The added command.
View Source
def bind_client(self: _MessageCommandGroupT, client: abc.Client, /) -> _MessageCommandGroupT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. super().bind_client(client) for command in self._commands: command.bind_client(client) return self
View Source
def bind_component(self: _MessageCommandGroupT, component: abc.Component, /) -> _MessageCommandGroupT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. super().bind_component(component) for command in self._commands: command.bind_component(component) return self
View Source
def find_command(self, content: str, /) -> collections.Iterable[tuple[str, abc.MessageCommand[typing.Any]]]: if self._is_strict: name = content.split(" ")[0] if command := self._names_to_commands.get(name): yield name, command return for command in self._commands: if (name_ := utilities.match_prefix_names(content, command.names)) is not None: yield name_, command
View Source
async def execute( self, ctx: abc.MessageContext, /, *, hooks: typing.Optional[collections.MutableSet[abc.MessageHooks]] = None, ) -> None: # <<inherited docstring from tanjun.abc.MessageCommand>>. if ctx.message.content is None: raise ValueError("Cannot execute a command with a content-less message") if self._hooks: if hooks is None: hooks = set() hooks.add(self._hooks) for name, command in self.find_command(ctx.content): if await command.check_context(ctx): content = ctx.content[len(name) :] lstripped_content = content.lstrip() space_len = len(content) - len(lstripped_content) ctx.set_triggering_name(ctx.triggering_name + (" " * space_len) + name) ctx.set_content(lstripped_content) await command.execute(ctx, hooks=hooks) return await super().execute(ctx, hooks=hooks)
View Source
class SlashCommand(BaseSlashCommand, abc.SlashCommand[abc.CommandCallbackSigT]): """Standard implementation of a slash command.""" __slots__ = ("_always_defer", "_builder", "_callback", "_client", "_tracked_options", "_wrapped_command") def __init__( self, callback: abc.CommandCallbackSigT, name: str, description: str, /, *, always_defer: bool = False, default_permission: bool = True, default_to_ephemeral: typing.Optional[bool] = None, is_global: bool = True, sort_options: bool = True, _wrapped_command: typing.Optional[abc.ExecutableCommand[typing.Any]] = None, ) -> None: r"""Initialise a slash command. .. note:: Under the standard implementation, `is_global` is used to determine whether the command should be bulk set by `tanjun.Client.set_global_commands` or when `set_global_commands` is True .. warning:: `default_permission` and `is_global` are ignored for commands within slash command groups. Parameters ---------- callback : collections.abc.Callable[[tanjun.abc.SlashContext, ...], collections.abc.Awaitable[None]] Callback to execute when the command is invoked. This should be an asynchronous callback which takes one positional argument of type `tanjun.abc.SlashContext`, returns `None` and may use dependency injection to access other services. name : str The command's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The command's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- always_defer : bool Whether the contexts this command is executed with should always be deferred before being passed to the command's callback. Defaults to `False`. .. note:: The ephemeral state of the first response is decided by whether the deferral is ephemeral. default_permission : bool Whether this command can be accessed without set permissions. Defaults to `True`, meaning that users can access the command by default. default_to_ephemeral : typing.Optional[bool] Whether this command's responses should default to ephemeral unless flags are set to override this. If this is left as `None` then the default set on the parent command(s), component or client will be in effect. is_global : bool Whether this command is a global command. Defaults to `True`. sort_options : bool Whether this command should sort its set options based on whether they're required. If this is `True` then the options are re-sorted to meet the requirement from Discord that required command options be listed before optional ones. Raises ------ ValueError Raises a value error for any of the following reasons: * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the command name has uppercase characters. * If the description is over 100 characters long. """ super().__init__(name, description, default_to_ephemeral=default_to_ephemeral, is_global=is_global) if not _wrapped_command and isinstance(callback, (abc.MessageCommand, abc.SlashCommand)): callback = typing.cast(abc.CommandCallbackSigT, callback.callback) self._always_defer = always_defer self._builder = _CommandBuilder(name, description, sort_options).set_default_permission(default_permission) self._callback = injecting.CallbackDescriptor[None](callback) self._client: typing.Optional[abc.Client] = None self._tracked_options: dict[str, _TrackedOption] = {} self._wrapped_command = _wrapped_command if typing.TYPE_CHECKING: __call__: abc.CommandCallbackSigT else: async def __call__(self, *args, **kwargs) -> None: await self._callback.callback(*args, **kwargs) @property def callback(self) -> abc.CommandCallbackSigT: # <<inherited docstring from tanjun.abc.SlashCommand>>. return typing.cast(abc.CommandCallbackSigT, self._callback.callback) @property def needs_injector(self) -> bool: return ( self._callback.needs_injector or any(option.needs_injector for option in self._tracked_options.values()) or super().needs_injector ) def bind_client(self: _SlashCommandT, client: abc.Client, /) -> _SlashCommandT: self._client = client super().bind_client(client) for option in self._tracked_options.values(): option.check_client(client) return self def build(self) -> special_endpoints_api.CommandBuilder: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. return self._builder.sort().copy() def load_into_component(self, component: abc.Component, /) -> None: super().load_into_component(component) if self._wrapped_command and isinstance(self._wrapped_command, components.AbstractComponentLoader): self._wrapped_command.load_into_component(component) def _add_option( self: _SlashCommandT, name: str, description: str, type_: typing.Union[hikari.OptionType, int] = hikari.OptionType.STRING, /, *, always_float: bool = False, channel_types: typing.Optional[collections.Sequence[int]] = None, choices: typing.Union[ collections.Mapping[str, typing.Union[str, int, float]], collections.Sequence[typing.Any], None ] = None, converters: typing.Union[collections.Iterable[ConverterSig], ConverterSig] = (), default: typing.Any = _UNDEFINED_DEFAULT, min_value: typing.Union[int, float, None] = None, max_value: typing.Union[int, float, None] = None, only_member: bool = False, pass_as_kwarg: bool = True, _stack_level: int = 0, ) -> _SlashCommandT: _validate_name(name) if len(description) > 100: raise ValueError("The option description cannot be over 100 characters in length") if len(self._builder.options) == 25: raise ValueError("Slash commands cannot have more than 25 options") if min_value and max_value and min_value > max_value: raise ValueError("The min value cannot be greater than the max value") type_ = hikari.OptionType(type_) if isinstance(converters, collections.Iterable): converters_ = list(map(_convert_to_injectable, converters)) else: converters_ = [_convert_to_injectable(converters)] if self._client: for converter in converters_: if isinstance(converter.callback, conversion.BaseConverter): converter.callback.check_client(self._client, f"{self._name}'s slash option '{name}'") if choices is None: actual_choices: typing.Optional[list[hikari.CommandChoice]] = None elif isinstance(choices, collections.Mapping): actual_choices = [hikari.CommandChoice(name=name, value=value) for name, value in choices.items()] else: warnings.warn( "Passing a sequence of tuples to `choices` is deprecated since 2.1.2a1, " "please pass a mapping instead.", category=DeprecationWarning, stacklevel=2 + _stack_level, ) actual_choices = [hikari.CommandChoice(name=name, value=value) for name, value in choices] if actual_choices and len(actual_choices) > 25: raise ValueError("Slash command options cannot have more than 25 choices") required = default is _UNDEFINED_DEFAULT self._builder.add_option( hikari.CommandOption( type=type_, name=name, description=description, is_required=required, choices=actual_choices, channel_types=channel_types, min_value=min_value, max_value=max_value, ) ) if pass_as_kwarg: self._tracked_options[name] = _TrackedOption( name=name, option_type=type_, always_float=always_float, converters=converters_, default=default, only_member=only_member, ) return self def add_str_option( self: _SlashCommandT, name: str, description: str, /, *, choices: typing.Union[collections.Mapping[str, str], collections.Sequence[str], None] = None, converters: typing.Union[collections.Sequence[ConverterSig], ConverterSig] = (), default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, _stack_level: int = 0, ) -> _SlashCommandT: r"""Add a string option to the slash command. .. note:: As a shorthand, `choices` also supports passing a list of strings rather than a dict of names to values (each string will used as both the choice's name and value with the names being capitalised). Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- choices : typing.Union[collections.abc.Mapping[str, str], collections.abc.Sequence[str], None] The option's choices. This either a mapping of [option_name, option_value] where both option_name and option_value should be strings of up to 100 characters or a sequence of strings where the string will be used for both the choice's name and value. converters : typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig] The option's converters. This may be either one or multiple `ConverterSig` callbacks used to convert the option's value to the final form. If no converters are provided then the raw value will be passed. Only the first converter to pass will be used. default : typing.Any The option's default value. If this is left as undefined then this option will be required. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used and the `coverters` field will be ignored. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the option has more than 25 choices. * If the command already has 25 options. """ if choices is None: actual_choices = None elif isinstance(choices, collections.Mapping): actual_choices = choices else: actual_choices = {} warned = False for choice in choices: if isinstance(choice, tuple): # type: ignore[unreachable] # the point of this is for deprecation if not warned: # type: ignore[unreachable] # mypy sees `warned = True` and messes up. warnings.warn( "Passing a sequence of tuples for 'choices' is deprecated since 2.1.2a1, " "please pass a mapping instead.", category=DeprecationWarning, stacklevel=2 + _stack_level, ) warned = True actual_choices[choice[0]] = choice[1] else: actual_choices[choice.capitalize()] = choice return self._add_option( name, description, hikari.OptionType.STRING, choices=actual_choices, converters=converters, default=default, pass_as_kwarg=pass_as_kwarg, ) def add_int_option( self: _SlashCommandT, name: str, description: str, /, *, choices: typing.Optional[collections.Mapping[str, int]] = None, converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (), default: typing.Any = _UNDEFINED_DEFAULT, min_value: typing.Optional[int] = None, max_value: typing.Optional[int] = None, pass_as_kwarg: bool = True, _stack_level: int = 0, ) -> _SlashCommandT: r"""Add an integer option to the slash command. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- choices : typing.Optional[collections.abc.Mapping[str, int]] The option's choices. This is a mapping of [option_name, option_value] where option_name should be a string of up to 100 characters and option_value should be an integer. converters : typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig, None] The option's converters. This may be either one or multiple `ConverterSig` callbacks used to convert the option's value to the final form. If no converters are provided then the raw value will be passed. Only the first converter to pass will be used. default : typing.Any The option's default value. If this is left as undefined then this option will be required. min_value : typing.Optional[int] The option's (inclusive) minimum value. Defaults to no minimum value. max_value : typing.Optional[int] The option's (inclusive) maximum value. Defaults to no minimum value. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used and the `coverters` field will be ignored. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the option has more than 25 choices. * If the command already has 25 options. * If `min_value` is greater than `max_value`. """ return self._add_option( name, description, hikari.OptionType.INTEGER, choices=choices, converters=converters, default=default, min_value=min_value, max_value=max_value, pass_as_kwarg=pass_as_kwarg, _stack_level=_stack_level + 1, ) def add_float_option( self: _SlashCommandT, name: str, description: str, /, *, always_float: bool = True, choices: typing.Optional[collections.Mapping[str, float]] = None, converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (), default: typing.Any = _UNDEFINED_DEFAULT, min_value: typing.Optional[float] = None, max_value: typing.Optional[float] = None, pass_as_kwarg: bool = True, _stack_level: int = 0, ) -> _SlashCommandT: r"""Add a float option to a slash command. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- always_float : bool If this is set to `True` then the value will always be converted to a float (this will happen before it's passed to converters). This masks behaviour from Discord where we will either be provided a `float` or `int` dependent on what the user provided and defaults to `True`. choices : typing.Optional[collections.abc.Mapping[str, float]] The option's choices. This is a mapping of [option_name, option_value] where option_name should be a string of up to 100 characters and option_value should be a float. converters : typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig, None] The option's converters. This may be either one or multiple `ConverterSig` callbacks used to convert the option's value to the final form. If no converters are provided then the raw value will be passed. Only the first converter to pass will be used. default : typing.Any The option's default value. If this is left as undefined then this option will be required. min_value : typing.Optional[float] The option's (inclusive) minimum value. Defaults to no minimum value. max_value : typing.Optional[float] The option's (inclusive) maximum value. Defaults to no minimum value. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used and the fields `coverters`, and `always_float` will be ignored. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the option has more than 25 choices. * If the command already has 25 options. * If `min_value` is greater than `max_value`. """ return self._add_option( name, description, hikari.OptionType.FLOAT, choices=choices, converters=converters, default=default, min_value=float(min_value) if min_value is not None else None, max_value=float(max_value) if max_value is not None else None, pass_as_kwarg=pass_as_kwarg, always_float=always_float, _stack_level=_stack_level + 1, ) def add_bool_option( self: _SlashCommandT, name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, ) -> _SlashCommandT: r"""Add a boolean option to a slash command. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- default : typing.Any The option's default value. If this is left as undefined then this option will be required. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the command already has 25 options. """ return self._add_option( name, description, hikari.OptionType.BOOLEAN, default=default, pass_as_kwarg=pass_as_kwarg ) def add_user_option( self: _SlashCommandT, name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, ) -> _SlashCommandT: r"""Add a user option to a slash command. .. note:: This may result in `hikari.InteractionMember` or `hikari.users.User` if the user isn't in the current guild or if this command was executed in a DM channel. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- default : typing.Any The option's default value. If this is left as undefined then this option will be required. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the option has more than 25 choices. * If the command already has 25 options. """ return self._add_option(name, description, hikari.OptionType.USER, default=default, pass_as_kwarg=pass_as_kwarg) def add_member_option( self: _SlashCommandT, name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, ) -> _SlashCommandT: r"""Add a member option to a slash command. .. note:: This will always result in `hikari.InteractionMember`. .. warning:: Unlike the other options, this is an artificial option which adds a restraint to the USER option type and therefore cannot have `pass_as_kwarg` set to `False` as this artificial constaint isn't present when its not being passed as a keyword argument. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- default : typing.Any The option's default value. If this is left as undefined then this option will be required. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the command already has 25 options. """ return self._add_option(name, description, hikari.OptionType.USER, default=default, only_member=True) def add_channel_option( self: _SlashCommandT, name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, types: typing.Optional[collections.Collection[type[hikari.PartialChannel]]] = None, pass_as_kwarg: bool = True, ) -> _SlashCommandT: r"""Add a channel option to a slash command. .. note:: This will always result in `hikari.InteractionChannel`. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Parameters ---------- default : typing.Any The option's default value. If this is left as undefined then this option will be required. types : typing.Optional[collections.abc.Collection[type[hikari.PartialChannel]]] A collection of the channel classes this option should accept. If left as `None` or empty then the option will allow all channel types. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the command already has 25 options. * If an invalid type is passed in `types`. """ import itertools if types: try: channel_types = list(set(itertools.chain.from_iterable(map(_channel_types.__getitem__, types)))) except KeyError as exc: raise ValueError(f"Unknown channel type {exc.args[0]}") from exc else: channel_types = None return self._add_option( name, description, hikari.OptionType.CHANNEL, channel_types=channel_types, default=default, pass_as_kwarg=pass_as_kwarg, ) def add_role_option( self: _SlashCommandT, name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, ) -> _SlashCommandT: r"""Add a role option to a slash command. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- default : typing.Any The option's default value. If this is left as undefined then this option will be required. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the command already has 25 options. """ return self._add_option(name, description, hikari.OptionType.ROLE, default=default, pass_as_kwarg=pass_as_kwarg) def add_mentionable_option( self: _SlashCommandT, name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, ) -> _SlashCommandT: r"""Add a mentionable option to a slash command. .. note:: This may target roles, guild members or users and results in `Union[hikari.User, hikari.InteractionMember, hikari.Role]`. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- default : typing.Any The option's default value. If this is left as undefined then this option will be required. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the command already has 25 options. """ return self._add_option( name, description, hikari.OptionType.MENTIONABLE, default=default, pass_as_kwarg=pass_as_kwarg ) async def _process_args(self, ctx: abc.SlashContext, /) -> collections.Mapping[str, typing.Any]: keyword_args: dict[str, typing.Union[int, float, str, hikari.User, hikari.Role, hikari.InteractionChannel]] = {} for tracked_option in self._tracked_options.values(): if not (option := ctx.options.get(tracked_option.name)): if tracked_option.default is _UNDEFINED_DEFAULT: raise RuntimeError( # TODO: ConversionError? f"Required option {tracked_option.name} is missing data, are you sure your commands" " are up to date?" ) else: keyword_args[tracked_option.name] = tracked_option.default elif option.type is hikari.OptionType.USER: member: typing.Optional[hikari.InteractionMember] = None if tracked_option.is_only_member and not (member := option.resolve_to_member(default=None)): raise errors.ConversionError( f"Couldn't find member for provided user: {option.value}", tracked_option.name ) keyword_args[option.name] = member or option.resolve_to_user() elif option.type is hikari.OptionType.CHANNEL: keyword_args[option.name] = option.resolve_to_channel() elif option.type is hikari.OptionType.ROLE: keyword_args[option.name] = option.resolve_to_role() elif option.type is hikari.OptionType.MENTIONABLE: keyword_args[option.name] = option.resolve_to_mentionable() else: value = option.value # To be type safe we obfuscate the fact that discord's double type will provide an int or float # depending on the value Discord inputs by always casting to float. if tracked_option.type is hikari.OptionType.FLOAT and tracked_option.is_always_float: value = float(value) if tracked_option.converters: value = await tracked_option.convert(ctx, option.value) keyword_args[option.name] = value return keyword_args async def execute( self, ctx: abc.SlashContext, /, option: typing.Optional[hikari.CommandInteractionOption] = None, *, hooks: typing.Optional[collections.MutableSet[abc.SlashHooks]] = None, ) -> None: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. if self._always_defer and not ctx.has_been_deferred and not ctx.has_responded: await ctx.defer() ctx = ctx.set_command(self) own_hooks = self._hooks or _EMPTY_HOOKS try: await own_hooks.trigger_pre_execution(ctx, hooks=hooks) if self._tracked_options: kwargs = await self._process_args(ctx) else: kwargs = _EMPTY_DICT await self._callback.resolve_with_command_context(ctx, ctx, **kwargs) except errors.CommandError as exc: await ctx.respond(exc.message) except errors.HaltExecution: # Unlike a message command, this won't necessarily reach the client level try except # block so we have to handle this here. await ctx.mark_not_found() except Exception as exc: if await own_hooks.trigger_error(ctx, exc, hooks=hooks) <= 0: raise else: await own_hooks.trigger_success(ctx, hooks=hooks) finally: await own_hooks.trigger_post_execution(ctx, hooks=hooks) def copy( self: _SlashCommandT, *, _new: bool = True, parent: typing.Optional[abc.SlashCommandGroup] = None ) -> _SlashCommandT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. if not _new: self._callback = copy.copy(self._callback) return super().copy(_new=_new, parent=parent) return super().copy(_new=_new, parent=parent)
Standard implementation of a slash command.
View Source
def __init__( self, callback: abc.CommandCallbackSigT, name: str, description: str, /, *, always_defer: bool = False, default_permission: bool = True, default_to_ephemeral: typing.Optional[bool] = None, is_global: bool = True, sort_options: bool = True, _wrapped_command: typing.Optional[abc.ExecutableCommand[typing.Any]] = None, ) -> None: r"""Initialise a slash command. .. note:: Under the standard implementation, `is_global` is used to determine whether the command should be bulk set by `tanjun.Client.set_global_commands` or when `set_global_commands` is True .. warning:: `default_permission` and `is_global` are ignored for commands within slash command groups. Parameters ---------- callback : collections.abc.Callable[[tanjun.abc.SlashContext, ...], collections.abc.Awaitable[None]] Callback to execute when the command is invoked. This should be an asynchronous callback which takes one positional argument of type `tanjun.abc.SlashContext`, returns `None` and may use dependency injection to access other services. name : str The command's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The command's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- always_defer : bool Whether the contexts this command is executed with should always be deferred before being passed to the command's callback. Defaults to `False`. .. note:: The ephemeral state of the first response is decided by whether the deferral is ephemeral. default_permission : bool Whether this command can be accessed without set permissions. Defaults to `True`, meaning that users can access the command by default. default_to_ephemeral : typing.Optional[bool] Whether this command's responses should default to ephemeral unless flags are set to override this. If this is left as `None` then the default set on the parent command(s), component or client will be in effect. is_global : bool Whether this command is a global command. Defaults to `True`. sort_options : bool Whether this command should sort its set options based on whether they're required. If this is `True` then the options are re-sorted to meet the requirement from Discord that required command options be listed before optional ones. Raises ------ ValueError Raises a value error for any of the following reasons: * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the command name has uppercase characters. * If the description is over 100 characters long. """ super().__init__(name, description, default_to_ephemeral=default_to_ephemeral, is_global=is_global) if not _wrapped_command and isinstance(callback, (abc.MessageCommand, abc.SlashCommand)): callback = typing.cast(abc.CommandCallbackSigT, callback.callback) self._always_defer = always_defer self._builder = _CommandBuilder(name, description, sort_options).set_default_permission(default_permission) self._callback = injecting.CallbackDescriptor[None](callback) self._client: typing.Optional[abc.Client] = None self._tracked_options: dict[str, _TrackedOption] = {} self._wrapped_command = _wrapped_command
Initialise a slash command.
Note:
Under the standard implementation, is_global is used to determine whether
the command should be bulk set by tanjun.Client.set_global_commands
or when set_global_commands is True
Warning:
default_permission and is_global are ignored for commands within
slash command groups.
Parameters
callback (collections.abc.Callable[[tanjun.abc.SlashContext, ...], collections.abc.Awaitable[None]]): Callback to execute when the command is invoked.
This should be an asynchronous callback which takes one positional argument of type
tanjun.abc.SlashContext, returnsNoneand may use dependency injection to access other services.name (str): The command's name.
This must match the regex
^[\w-]{1,32}in Unicode mode and be lowercase.- description (str): The command's description. This should be inclusively between 1-100 characters in length.
Other Parameters
always_defer (bool): Whether the contexts this command is executed with should always be deferred before being passed to the command's callback.
Defaults to
False.Note: The ephemeral state of the first response is decided by whether the deferral is ephemeral.
default_permission (bool): Whether this command can be accessed without set permissions.
Defaults to
True, meaning that users can access the command by default.default_to_ephemeral (typing.Optional[bool]): Whether this command's responses should default to ephemeral unless flags are set to override this.
If this is left as
Nonethen the default set on the parent command(s), component or client will be in effect.- is_global (bool):
Whether this command is a global command. Defaults to
True. sort_options (bool): Whether this command should sort its set options based on whether they're required.
If this is
Truethen the options are re-sorted to meet the requirement from Discord that required command options be listed before optional ones.
Raises
- ValueError: Raises a value error for any of the following reasons:
- If the command name doesn't match the regex
^[\w-]{1,32}$(Unicode mode). - If the command name has uppercase characters.
- If the description is over 100 characters long.
- If the command name doesn't match the regex
Callback which is called during execution.
View Source
def bind_client(self: _SlashCommandT, client: abc.Client, /) -> _SlashCommandT: self._client = client super().bind_client(client) for option in self._tracked_options.values(): option.check_client(client) return self
View Source
def build(self) -> special_endpoints_api.CommandBuilder: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. return self._builder.sort().copy()
Get a builder object for this command.
Returns
- hikari.api.CommandBuilder: A builder object for this command. Use to declare this command on globally or for a specific guild.
View Source
def load_into_component(self, component: abc.Component, /) -> None: super().load_into_component(component) if self._wrapped_command and isinstance(self._wrapped_command, components.AbstractComponentLoader): self._wrapped_command.load_into_component(component)
Load the object into the component.
Parameters
- component (tanjun.abc.Component): The component this object should be loaded into.
View Source
def add_str_option( self: _SlashCommandT, name: str, description: str, /, *, choices: typing.Union[collections.Mapping[str, str], collections.Sequence[str], None] = None, converters: typing.Union[collections.Sequence[ConverterSig], ConverterSig] = (), default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, _stack_level: int = 0, ) -> _SlashCommandT: r"""Add a string option to the slash command. .. note:: As a shorthand, `choices` also supports passing a list of strings rather than a dict of names to values (each string will used as both the choice's name and value with the names being capitalised). Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- choices : typing.Union[collections.abc.Mapping[str, str], collections.abc.Sequence[str], None] The option's choices. This either a mapping of [option_name, option_value] where both option_name and option_value should be strings of up to 100 characters or a sequence of strings where the string will be used for both the choice's name and value. converters : typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig] The option's converters. This may be either one or multiple `ConverterSig` callbacks used to convert the option's value to the final form. If no converters are provided then the raw value will be passed. Only the first converter to pass will be used. default : typing.Any The option's default value. If this is left as undefined then this option will be required. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used and the `coverters` field will be ignored. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the option has more than 25 choices. * If the command already has 25 options. """ if choices is None: actual_choices = None elif isinstance(choices, collections.Mapping): actual_choices = choices else: actual_choices = {} warned = False for choice in choices: if isinstance(choice, tuple): # type: ignore[unreachable] # the point of this is for deprecation if not warned: # type: ignore[unreachable] # mypy sees `warned = True` and messes up. warnings.warn( "Passing a sequence of tuples for 'choices' is deprecated since 2.1.2a1, " "please pass a mapping instead.", category=DeprecationWarning, stacklevel=2 + _stack_level, ) warned = True actual_choices[choice[0]] = choice[1] else: actual_choices[choice.capitalize()] = choice return self._add_option( name, description, hikari.OptionType.STRING, choices=actual_choices, converters=converters, default=default, pass_as_kwarg=pass_as_kwarg, )
Add a string option to the slash command.
Note:
As a shorthand, choices also supports passing a list of strings
rather than a dict of names to values (each string will used as
both the choice's name and value with the names being capitalised).
Parameters
name (str): The option's name.
This must match the regex
^[\w-]{1,32}in Unicode mode and be lowercase.- description (str): The option's description. This should be inclusively between 1-100 characters in length.
Other Parameters
choices (typing.Union[collections.abc.Mapping[str, str], collections.abc.Sequence[str], None]): The option's choices.
This either a mapping of [option_name, option_value] where both option_name and option_value should be strings of up to 100 characters or a sequence of strings where the string will be used for both the choice's name and value.
converters (typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig]): The option's converters.
This may be either one or multiple
ConverterSigcallbacks used to convert the option's value to the final form. If no converters are provided then the raw value will be passed.Only the first converter to pass will be used.
- default (typing.Any): The option's default value. If this is left as undefined then this option will be required.
pass_as_kwarg (bool): Whether or not to pass this option as a keyword argument to the command callback.
Defaults to
True. IfFalseis passed here thendefaultwill only decide whether the option is required without the actual value being used and thecovertersfield will be ignored.
Returns
- Self: The command object for chaining.
Raises
- ValueError: Raises a value error for any of the following reasons:
- If the option name doesn't match the regex
^[\w-]{1,32}$(Unicode mode). - If the option name has uppercase characters.
- If the option description is over 100 characters in length.
- If the option has more than 25 choices.
- If the command already has 25 options.
- If the option name doesn't match the regex
View Source
def add_int_option( self: _SlashCommandT, name: str, description: str, /, *, choices: typing.Optional[collections.Mapping[str, int]] = None, converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (), default: typing.Any = _UNDEFINED_DEFAULT, min_value: typing.Optional[int] = None, max_value: typing.Optional[int] = None, pass_as_kwarg: bool = True, _stack_level: int = 0, ) -> _SlashCommandT: r"""Add an integer option to the slash command. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- choices : typing.Optional[collections.abc.Mapping[str, int]] The option's choices. This is a mapping of [option_name, option_value] where option_name should be a string of up to 100 characters and option_value should be an integer. converters : typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig, None] The option's converters. This may be either one or multiple `ConverterSig` callbacks used to convert the option's value to the final form. If no converters are provided then the raw value will be passed. Only the first converter to pass will be used. default : typing.Any The option's default value. If this is left as undefined then this option will be required. min_value : typing.Optional[int] The option's (inclusive) minimum value. Defaults to no minimum value. max_value : typing.Optional[int] The option's (inclusive) maximum value. Defaults to no minimum value. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used and the `coverters` field will be ignored. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the option has more than 25 choices. * If the command already has 25 options. * If `min_value` is greater than `max_value`. """ return self._add_option( name, description, hikari.OptionType.INTEGER, choices=choices, converters=converters, default=default, min_value=min_value, max_value=max_value, pass_as_kwarg=pass_as_kwarg, _stack_level=_stack_level + 1, )
Add an integer option to the slash command.
Parameters
name (str): The option's name.
This must match the regex
^[\w-]{1,32}in Unicode mode and be lowercase.- description (str): The option's description. This should be inclusively between 1-100 characters in length.
Other Parameters
choices (typing.Optional[collections.abc.Mapping[str, int]]): The option's choices.
This is a mapping of [option_name, option_value] where option_name should be a string of up to 100 characters and option_value should be an integer.
converters (typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig, None]): The option's converters.
This may be either one or multiple
ConverterSigcallbacks used to convert the option's value to the final form. If no converters are provided then the raw value will be passed.Only the first converter to pass will be used.
- default (typing.Any): The option's default value. If this is left as undefined then this option will be required.
min_value (typing.Optional[int]): The option's (inclusive) minimum value.
Defaults to no minimum value.
max_value (typing.Optional[int]): The option's (inclusive) maximum value.
Defaults to no minimum value.
pass_as_kwarg (bool): Whether or not to pass this option as a keyword argument to the command callback.
Defaults to
True. IfFalseis passed here thendefaultwill only decide whether the option is required without the actual value being used and thecovertersfield will be ignored.
Returns
- Self: The command object for chaining.
Raises
- ValueError: Raises a value error for any of the following reasons:
- If the option name doesn't match the regex
^[\w-]{1,32}$(Unicode mode). - If the option name has uppercase characters.
- If the option description is over 100 characters in length.
- If the option has more than 25 choices.
- If the command already has 25 options.
- If
min_valueis greater thanmax_value.
- If the option name doesn't match the regex
View Source
def add_float_option( self: _SlashCommandT, name: str, description: str, /, *, always_float: bool = True, choices: typing.Optional[collections.Mapping[str, float]] = None, converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (), default: typing.Any = _UNDEFINED_DEFAULT, min_value: typing.Optional[float] = None, max_value: typing.Optional[float] = None, pass_as_kwarg: bool = True, _stack_level: int = 0, ) -> _SlashCommandT: r"""Add a float option to a slash command. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- always_float : bool If this is set to `True` then the value will always be converted to a float (this will happen before it's passed to converters). This masks behaviour from Discord where we will either be provided a `float` or `int` dependent on what the user provided and defaults to `True`. choices : typing.Optional[collections.abc.Mapping[str, float]] The option's choices. This is a mapping of [option_name, option_value] where option_name should be a string of up to 100 characters and option_value should be a float. converters : typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig, None] The option's converters. This may be either one or multiple `ConverterSig` callbacks used to convert the option's value to the final form. If no converters are provided then the raw value will be passed. Only the first converter to pass will be used. default : typing.Any The option's default value. If this is left as undefined then this option will be required. min_value : typing.Optional[float] The option's (inclusive) minimum value. Defaults to no minimum value. max_value : typing.Optional[float] The option's (inclusive) maximum value. Defaults to no minimum value. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used and the fields `coverters`, and `always_float` will be ignored. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the option has more than 25 choices. * If the command already has 25 options. * If `min_value` is greater than `max_value`. """ return self._add_option( name, description, hikari.OptionType.FLOAT, choices=choices, converters=converters, default=default, min_value=float(min_value) if min_value is not None else None, max_value=float(max_value) if max_value is not None else None, pass_as_kwarg=pass_as_kwarg, always_float=always_float, _stack_level=_stack_level + 1, )
Add a float option to a slash command.
Parameters
name (str): The option's name.
This must match the regex
^[\w-]{1,32}in Unicode mode and be lowercase.- description (str): The option's description. This should be inclusively between 1-100 characters in length.
Other Parameters
always_float (bool): If this is set to
Truethen the value will always be converted to a float (this will happen before it's passed to converters).This masks behaviour from Discord where we will either be provided a
floatorintdependent on what the user provided and defaults toTrue.choices (typing.Optional[collections.abc.Mapping[str, float]]): The option's choices.
This is a mapping of [option_name, option_value] where option_name should be a string of up to 100 characters and option_value should be a float.
converters (typing.Union[collections.abc.Sequence[ConverterSig], ConverterSig, None]): The option's converters.
This may be either one or multiple
ConverterSigcallbacks used to convert the option's value to the final form. If no converters are provided then the raw value will be passed.Only the first converter to pass will be used.
- default (typing.Any): The option's default value. If this is left as undefined then this option will be required.
min_value (typing.Optional[float]): The option's (inclusive) minimum value.
Defaults to no minimum value.
max_value (typing.Optional[float]): The option's (inclusive) maximum value.
Defaults to no minimum value.
pass_as_kwarg (bool): Whether or not to pass this option as a keyword argument to the command callback.
Defaults to
True. IfFalseis passed here thendefaultwill only decide whether the option is required without the actual value being used and the fieldscoverters, andalways_floatwill be ignored.
Returns
- Self: The command object for chaining.
Raises
- ValueError: Raises a value error for any of the following reasons:
- If the option name doesn't match the regex
^[\w-]{1,32}$(Unicode mode). - If the option name has uppercase characters.
- If the option description is over 100 characters in length.
- If the option has more than 25 choices.
- If the command already has 25 options.
- If
min_valueis greater thanmax_value.
- If the option name doesn't match the regex
View Source
def add_bool_option( self: _SlashCommandT, name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, ) -> _SlashCommandT: r"""Add a boolean option to a slash command. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- default : typing.Any The option's default value. If this is left as undefined then this option will be required. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the command already has 25 options. """ return self._add_option( name, description, hikari.OptionType.BOOLEAN, default=default, pass_as_kwarg=pass_as_kwarg )
Add a boolean option to a slash command.
Parameters
name (str): The option's name.
This must match the regex
^[\w-]{1,32}in Unicode mode and be lowercase.- description (str): The option's description. This should be inclusively between 1-100 characters in length.
Other Parameters
- default (typing.Any): The option's default value. If this is left as undefined then this option will be required.
pass_as_kwarg (bool): Whether or not to pass this option as a keyword argument to the command callback.
Defaults to
True. IfFalseis passed here thendefaultwill only decide whether the option is required without the actual value being used.
Returns
- Self: The command object for chaining.
Raises
- ValueError: Raises a value error for any of the following reasons:
- If the option name doesn't match the regex
^[\w-]{1,32}$(Unicode mode). - If the option name has uppercase characters.
- If the option description is over 100 characters in length.
- If the command already has 25 options.
- If the option name doesn't match the regex
View Source
def add_user_option( self: _SlashCommandT, name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, ) -> _SlashCommandT: r"""Add a user option to a slash command. .. note:: This may result in `hikari.InteractionMember` or `hikari.users.User` if the user isn't in the current guild or if this command was executed in a DM channel. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- default : typing.Any The option's default value. If this is left as undefined then this option will be required. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the option has more than 25 choices. * If the command already has 25 options. """ return self._add_option(name, description, hikari.OptionType.USER, default=default, pass_as_kwarg=pass_as_kwarg)
Add a user option to a slash command.
Note:
This may result in hikari.InteractionMember or
hikari.users.User if the user isn't in the current guild or if this
command was executed in a DM channel.
Parameters
name (str): The option's name.
This must match the regex
^[\w-]{1,32}in Unicode mode and be lowercase.- description (str): The option's description. This should be inclusively between 1-100 characters in length.
Other Parameters
- default (typing.Any): The option's default value. If this is left as undefined then this option will be required.
pass_as_kwarg (bool): Whether or not to pass this option as a keyword argument to the command callback.
Defaults to
True. IfFalseis passed here thendefaultwill only decide whether the option is required without the actual value being used.
Returns
- Self: The command object for chaining.
Raises
- ValueError: Raises a value error for any of the following reasons:
- If the option name doesn't match the regex
^[\w-]{1,32}$(Unicode mode). - If the option name has uppercase characters.
- If the option description is over 100 characters in length.
- If the option has more than 25 choices.
- If the command already has 25 options.
- If the option name doesn't match the regex
View Source
def add_member_option( self: _SlashCommandT, name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, ) -> _SlashCommandT: r"""Add a member option to a slash command. .. note:: This will always result in `hikari.InteractionMember`. .. warning:: Unlike the other options, this is an artificial option which adds a restraint to the USER option type and therefore cannot have `pass_as_kwarg` set to `False` as this artificial constaint isn't present when its not being passed as a keyword argument. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- default : typing.Any The option's default value. If this is left as undefined then this option will be required. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the command already has 25 options. """ return self._add_option(name, description, hikari.OptionType.USER, default=default, only_member=True)
Add a member option to a slash command.
Note:
This will always result in hikari.InteractionMember.
Warning:
Unlike the other options, this is an artificial option which adds
a restraint to the USER option type and therefore cannot have
pass_as_kwarg set to False as this artificial constaint isn't
present when its not being passed as a keyword argument.
Parameters
name (str): The option's name.
This must match the regex
^[\w-]{1,32}in Unicode mode and be lowercase.- description (str): The option's description. This should be inclusively between 1-100 characters in length.
Other Parameters
- default (typing.Any): The option's default value. If this is left as undefined then this option will be required.
Returns
- Self: The command object for chaining.
Raises
- ValueError: Raises a value error for any of the following reasons:
- If the option name doesn't match the regex
^[\w-]{1,32}$(Unicode mode). - If the option name has uppercase characters.
- If the option description is over 100 characters in length.
- If the command already has 25 options.
- If the option name doesn't match the regex
View Source
def add_channel_option( self: _SlashCommandT, name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, types: typing.Optional[collections.Collection[type[hikari.PartialChannel]]] = None, pass_as_kwarg: bool = True, ) -> _SlashCommandT: r"""Add a channel option to a slash command. .. note:: This will always result in `hikari.InteractionChannel`. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Parameters ---------- default : typing.Any The option's default value. If this is left as undefined then this option will be required. types : typing.Optional[collections.abc.Collection[type[hikari.PartialChannel]]] A collection of the channel classes this option should accept. If left as `None` or empty then the option will allow all channel types. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the command already has 25 options. * If an invalid type is passed in `types`. """ import itertools if types: try: channel_types = list(set(itertools.chain.from_iterable(map(_channel_types.__getitem__, types)))) except KeyError as exc: raise ValueError(f"Unknown channel type {exc.args[0]}") from exc else: channel_types = None return self._add_option( name, description, hikari.OptionType.CHANNEL, channel_types=channel_types, default=default, pass_as_kwarg=pass_as_kwarg, )
Add a channel option to a slash command.
Note:
This will always result in hikari.InteractionChannel.
Parameters
name (str): The option's name.
This must match the regex
^[\w-]{1,32}in Unicode mode and be lowercase.- description (str): The option's description. This should be inclusively between 1-100 characters in length.
Parameters
- default (typing.Any): The option's default value. If this is left as undefined then this option will be required.
types (typing.Optional[collections.abc.Collection[type[hikari.PartialChannel]]]): A collection of the channel classes this option should accept.
If left as
Noneor empty then the option will allow all channel types.pass_as_kwarg (bool): Whether or not to pass this option as a keyword argument to the command callback.
Defaults to
True. IfFalseis passed here thendefaultwill only decide whether the option is required without the actual value being used.
Returns
- Self: The command object for chaining.
Raises
- ValueError: Raises a value error for any of the following reasons:
- If the option name doesn't match the regex
^[\w-]{1,32}$(Unicode mode). - If the option name has uppercase characters.
- If the option description is over 100 characters in length.
- If the command already has 25 options.
- If an invalid type is passed in
types.
- If the option name doesn't match the regex
View Source
def add_role_option( self: _SlashCommandT, name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, ) -> _SlashCommandT: r"""Add a role option to a slash command. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- default : typing.Any The option's default value. If this is left as undefined then this option will be required. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the command already has 25 options. """ return self._add_option(name, description, hikari.OptionType.ROLE, default=default, pass_as_kwarg=pass_as_kwarg)
Add a role option to a slash command.
Parameters
name (str): The option's name.
This must match the regex
^[\w-]{1,32}in Unicode mode and be lowercase.- description (str): The option's description. This should be inclusively between 1-100 characters in length.
Other Parameters
- default (typing.Any): The option's default value. If this is left as undefined then this option will be required.
pass_as_kwarg (bool): Whether or not to pass this option as a keyword argument to the command callback.
Defaults to
True. IfFalseis passed here thendefaultwill only decide whether the option is required without the actual value being used.
Returns
- Self: The command object for chaining.
Raises
- ValueError: Raises a value error for any of the following reasons:
- If the option name doesn't match the regex
^[\w-]{1,32}$(Unicode mode). - If the option name has uppercase characters.
- If the option description is over 100 characters in length.
- If the command already has 25 options.
- If the option name doesn't match the regex
View Source
def add_mentionable_option( self: _SlashCommandT, name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, ) -> _SlashCommandT: r"""Add a mentionable option to a slash command. .. note:: This may target roles, guild members or users and results in `Union[hikari.User, hikari.InteractionMember, hikari.Role]`. Parameters ---------- name : str The option's name. This must match the regex `^[\w-]{1,32}` in Unicode mode and be lowercase. description : str The option's description. This should be inclusively between 1-100 characters in length. Other Parameters ---------------- default : typing.Any The option's default value. If this is left as undefined then this option will be required. pass_as_kwarg : bool Whether or not to pass this option as a keyword argument to the command callback. Defaults to `True`. If `False` is passed here then `default` will only decide whether the option is required without the actual value being used. Returns ------- Self The command object for chaining. Raises ------ ValueError Raises a value error for any of the following reasons: * If the option name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the option name has uppercase characters. * If the option description is over 100 characters in length. * If the command already has 25 options. """ return self._add_option( name, description, hikari.OptionType.MENTIONABLE, default=default, pass_as_kwarg=pass_as_kwarg )
Add a mentionable option to a slash command.
Note:
This may target roles, guild members or users and results in
Union[hikari.User, hikari.InteractionMember, hikari.Role].
Parameters
name (str): The option's name.
This must match the regex
^[\w-]{1,32}in Unicode mode and be lowercase.- description (str): The option's description. This should be inclusively between 1-100 characters in length.
Other Parameters
- default (typing.Any): The option's default value. If this is left as undefined then this option will be required.
pass_as_kwarg (bool): Whether or not to pass this option as a keyword argument to the command callback.
Defaults to
True. IfFalseis passed here thendefaultwill only decide whether the option is required without the actual value being used.
Returns
- Self: The command object for chaining.
Raises
- ValueError: Raises a value error for any of the following reasons:
- If the option name doesn't match the regex
^[\w-]{1,32}$(Unicode mode). - If the option name has uppercase characters.
- If the option description is over 100 characters in length.
- If the command already has 25 options.
- If the option name doesn't match the regex
View Source
async def execute( self, ctx: abc.SlashContext, /, option: typing.Optional[hikari.CommandInteractionOption] = None, *, hooks: typing.Optional[collections.MutableSet[abc.SlashHooks]] = None, ) -> None: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. if self._always_defer and not ctx.has_been_deferred and not ctx.has_responded: await ctx.defer() ctx = ctx.set_command(self) own_hooks = self._hooks or _EMPTY_HOOKS try: await own_hooks.trigger_pre_execution(ctx, hooks=hooks) if self._tracked_options: kwargs = await self._process_args(ctx) else: kwargs = _EMPTY_DICT await self._callback.resolve_with_command_context(ctx, ctx, **kwargs) except errors.CommandError as exc: await ctx.respond(exc.message) except errors.HaltExecution: # Unlike a message command, this won't necessarily reach the client level try except # block so we have to handle this here. await ctx.mark_not_found() except Exception as exc: if await own_hooks.trigger_error(ctx, exc, hooks=hooks) <= 0: raise else: await own_hooks.trigger_success(ctx, hooks=hooks) finally: await own_hooks.trigger_post_execution(ctx, hooks=hooks)
View Source
def copy( self: _SlashCommandT, *, _new: bool = True, parent: typing.Optional[abc.SlashCommandGroup] = None ) -> _SlashCommandT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. if not _new: self._callback = copy.copy(self._callback) return super().copy(_new=_new, parent=parent) return super().copy(_new=_new, parent=parent)
Create a copy of this command.
Returns
- Self: A copy of this command.
Inherited Members
View Source
class SlashCommandGroup(BaseSlashCommand, abc.SlashCommandGroup): """Standard implementation of a slash command group. .. note:: Unlike message command grups, slash command groups cannot be callable functions themselves. """ __slots__ = ("_commands", "_default_permission") def __init__( self, name: str, description: str, /, *, default_to_ephemeral: typing.Optional[bool] = None, default_permission: bool = True, is_global: bool = True, ) -> None: r"""Initialise a slash command group. .. note:: Under the standard implementation, `is_global` is used to determine whether the command should be bulk set by `tanjun.Client.set_global_commands` or when `set_global_commands` is True Parameters ---------- name : str The name of the command group. This must match the regex `^[\w-]{1,32}$` in Unicode mode and be lowercase. description : str The description of the command group. Other Parameters ---------------- default_permission : bool Whether this command can be accessed without set permissions. Defaults to `True`, meaning that users can access the command by default. default_to_ephemeral : typing.Optional[bool] Whether this command's responses should default to ephemeral unless flags are set to override this. If this is left as `None` then the default set on the parent command(s), component or client will be in effect. is_global : bool Whether this command is a global command. Defaults to `True`. Raises ------ ValueError Raises a value error for any of the following reasons: * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the command name has uppercase characters. * If the description is over 100 characters long. """ super().__init__(name, description, default_to_ephemeral=default_to_ephemeral, is_global=is_global) self._commands: dict[str, abc.BaseSlashCommand] = {} self._default_permission = default_permission @property def commands(self) -> collections.Collection[abc.BaseSlashCommand]: # <<inherited docstring from tanjun.abc.SlashCommandGroup>>. return self._commands.copy().values() def build(self) -> special_endpoints_api.CommandBuilder: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. builder = _CommandBuilder(self._name, self._description, False).set_default_permission(self._default_permission) for command in self._commands.values(): option_type = ( hikari.OptionType.SUB_COMMAND_GROUP if isinstance(command, abc.SlashCommandGroup) else hikari.OptionType.SUB_COMMAND ) command_builder = command.build() builder.add_option( hikari.CommandOption( type=option_type, name=command.name, description=command_builder.description, is_required=False, options=command_builder.options, ) ) return builder def copy( self: _SlashCommandGroupT, *, _new: bool = True, parent: typing.Optional[abc.SlashCommandGroup] = None ) -> _SlashCommandGroupT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. if not _new: self._commands = {name: command.copy() for name, command in self._commands.items()} return super().copy(_new=_new, parent=parent) return super().copy(_new=_new, parent=parent) def add_command(self: _SlashCommandGroupT, command: abc.BaseSlashCommand, /) -> _SlashCommandGroupT: """Add a slash command to this group. .. warning:: Command groups are only supported within top-level groups. Parameters ---------- command : tanjun.abc.BaseSlashCommand Command to add to this group. Returns ------- Self Object of this group to enable chained calls. """ if self._parent and isinstance(command, abc.SlashCommandGroup): raise ValueError("Cannot add a slash command group to a nested slash command group") if len(self._commands) == 25: raise ValueError("Cannot add more than 25 commands to a slash command group") if command.name in self._commands: raise ValueError(f"Command with name {command.name!r} already exists in this group") command.set_parent(self) self._commands[command.name] = command return self def remove_command(self: _SlashCommandGroupT, command: abc.BaseSlashCommand, /) -> _SlashCommandGroupT: """Remove a command from this group. Parameters ---------- command : tanjun.abc.BaseSlashCommand Command to remove from this group. Returns ------- Self Object of this group to enable chained calls. """ del self._commands[command.name] return self def with_command(self, command: abc.BaseSlashCommandT, /) -> abc.BaseSlashCommandT: """Add a slash command to this group through a decorator call. Parameters ---------- command : tanjun.abc.BaseSlashCommand Command to add to this group. Returns ------- tanjun.abc.BaseSlashCommand Command which was added to this group. """ self.add_command(command) return command async def execute( self, ctx: abc.SlashContext, /, option: typing.Optional[hikari.CommandInteractionOption] = None, *, hooks: typing.Optional[collections.MutableSet[abc.SlashHooks]] = None, ) -> None: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. if not option and ctx.interaction.options: option = ctx.interaction.options[0] elif option and option.options: option = option.options[0] else: raise RuntimeError("Missing sub-command option") if command := self._commands.get(option.name): if command.defaults_to_ephemeral is not None: ctx.set_ephemeral_default(command.defaults_to_ephemeral) if await command.check_context(ctx): await command.execute(ctx, option=option, hooks=hooks) return await ctx.mark_not_found()
Standard implementation of a slash command group.
Note: Unlike message command grups, slash command groups cannot be callable functions themselves.
View Source
def __init__( self, name: str, description: str, /, *, default_to_ephemeral: typing.Optional[bool] = None, default_permission: bool = True, is_global: bool = True, ) -> None: r"""Initialise a slash command group. .. note:: Under the standard implementation, `is_global` is used to determine whether the command should be bulk set by `tanjun.Client.set_global_commands` or when `set_global_commands` is True Parameters ---------- name : str The name of the command group. This must match the regex `^[\w-]{1,32}$` in Unicode mode and be lowercase. description : str The description of the command group. Other Parameters ---------------- default_permission : bool Whether this command can be accessed without set permissions. Defaults to `True`, meaning that users can access the command by default. default_to_ephemeral : typing.Optional[bool] Whether this command's responses should default to ephemeral unless flags are set to override this. If this is left as `None` then the default set on the parent command(s), component or client will be in effect. is_global : bool Whether this command is a global command. Defaults to `True`. Raises ------ ValueError Raises a value error for any of the following reasons: * If the command name doesn't match the regex `^[\w-]{1,32}$` (Unicode mode). * If the command name has uppercase characters. * If the description is over 100 characters long. """ super().__init__(name, description, default_to_ephemeral=default_to_ephemeral, is_global=is_global) self._commands: dict[str, abc.BaseSlashCommand] = {} self._default_permission = default_permission
Initialise a slash command group.
Note:
Under the standard implementation, is_global is used to determine
whether the command should be bulk set by tanjun.Client.set_global_commands
or when set_global_commands is True
Parameters
name (str): The name of the command group.
This must match the regex
^[\w-]{1,32}$in Unicode mode and be lowercase.- description (str): The description of the command group.
Other Parameters
default_permission (bool): Whether this command can be accessed without set permissions.
Defaults to
True, meaning that users can access the command by default.default_to_ephemeral (typing.Optional[bool]): Whether this command's responses should default to ephemeral unless flags are set to override this.
If this is left as
Nonethen the default set on the parent command(s), component or client will be in effect.- is_global (bool):
Whether this command is a global command. Defaults to
True.
Raises
- ValueError: Raises a value error for any of the following reasons:
- If the command name doesn't match the regex
^[\w-]{1,32}$(Unicode mode). - If the command name has uppercase characters.
- If the description is over 100 characters long.
- If the command name doesn't match the regex
Collection of the commands in this group.
View Source
def build(self) -> special_endpoints_api.CommandBuilder: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. builder = _CommandBuilder(self._name, self._description, False).set_default_permission(self._default_permission) for command in self._commands.values(): option_type = ( hikari.OptionType.SUB_COMMAND_GROUP if isinstance(command, abc.SlashCommandGroup) else hikari.OptionType.SUB_COMMAND ) command_builder = command.build() builder.add_option( hikari.CommandOption( type=option_type, name=command.name, description=command_builder.description, is_required=False, options=command_builder.options, ) ) return builder
Get a builder object for this command.
Returns
- hikari.api.CommandBuilder: A builder object for this command. Use to declare this command on globally or for a specific guild.
View Source
def copy( self: _SlashCommandGroupT, *, _new: bool = True, parent: typing.Optional[abc.SlashCommandGroup] = None ) -> _SlashCommandGroupT: # <<inherited docstring from tanjun.abc.ExecutableCommand>>. if not _new: self._commands = {name: command.copy() for name, command in self._commands.items()} return super().copy(_new=_new, parent=parent) return super().copy(_new=_new, parent=parent)
Create a copy of this command.
Returns
- Self: A copy of this command.
View Source
def add_command(self: _SlashCommandGroupT, command: abc.BaseSlashCommand, /) -> _SlashCommandGroupT: """Add a slash command to this group. .. warning:: Command groups are only supported within top-level groups. Parameters ---------- command : tanjun.abc.BaseSlashCommand Command to add to this group. Returns ------- Self Object of this group to enable chained calls. """ if self._parent and isinstance(command, abc.SlashCommandGroup): raise ValueError("Cannot add a slash command group to a nested slash command group") if len(self._commands) == 25: raise ValueError("Cannot add more than 25 commands to a slash command group") if command.name in self._commands: raise ValueError(f"Command with name {command.name!r} already exists in this group") command.set_parent(self) self._commands[command.name] = command return self
Add a slash command to this group.
Warning: Command groups are only supported within top-level groups.
Parameters
- command (tanjun.abc.BaseSlashCommand): Command to add to this group.
Returns
- Self: Object of this group to enable chained calls.
View Source
def remove_command(self: _SlashCommandGroupT, command: abc.BaseSlashCommand, /) -> _SlashCommandGroupT: """Remove a command from this group. Parameters ---------- command : tanjun.abc.BaseSlashCommand Command to remove from this group. Returns ------- Self Object of this group to enable chained calls. """ del self._commands[command.name] return self
Remove a command from this group.
Parameters
- command (tanjun.abc.BaseSlashCommand): Command to remove from this group.
Returns
- Self: Object of this group to enable chained calls.
View Source
def with_command(self, command: abc.BaseSlashCommandT, /) -> abc.BaseSlashCommandT: """Add a slash command to this group through a decorator call. Parameters ---------- command : tanjun.abc.BaseSlashCommand Command to add to this group. Returns ------- tanjun.abc.BaseSlashCommand Command which was added to this group. """ self.add_command(command) return command
Add a slash command to this group through a decorator call.
Parameters
- command (tanjun.abc.BaseSlashCommand): Command to add to this group.
Returns
- tanjun.abc.BaseSlashCommand: Command which was added to this group.
View Source
async def execute( self, ctx: abc.SlashContext, /, option: typing.Optional[hikari.CommandInteractionOption] = None, *, hooks: typing.Optional[collections.MutableSet[abc.SlashHooks]] = None, ) -> None: # <<inherited docstring from tanjun.abc.BaseSlashCommand>>. if not option and ctx.interaction.options: option = ctx.interaction.options[0] elif option and option.options: option = option.options[0] else: raise RuntimeError("Missing sub-command option") if command := self._commands.get(option.name): if command.defaults_to_ephemeral is not None: ctx.set_ephemeral_default(command.defaults_to_ephemeral) if await command.check_context(ctx): await command.execute(ctx, option=option, hooks=hooks) return await ctx.mark_not_found()
Inherited Members
View Source
def with_str_slash_option( name: str, description: str, /, *, choices: typing.Union[collections.Mapping[str, str], collections.Sequence[str], None] = None, converters: typing.Union[collections.Sequence[ConverterSig], ConverterSig] = (), default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, ) -> collections.Callable[[_SlashCommandT], _SlashCommandT]: """Add a string option to a slash command. For more information on this function's parameters see `SlashCommand.add_str_option`. Examples -------- ```py @with_str_slash_option("name", "A name.") @as_slash_command("command", "A command") async def command(self, ctx: tanjun.abc.SlashContext, name: str) -> None: ... ``` Returns ------- collections.abc.Callable[[_SlashCommandT], _SlashCommandT] Decorator callback which adds the option to the command. """ return lambda c: c.add_str_option( name, description, default=default, choices=choices, converters=converters, pass_as_kwarg=pass_as_kwarg, _stack_level=1, )
Add a string option to a slash command.
For more information on this function's parameters see SlashCommand.add_str_option.
Examples
@with_str_slash_option("name", "A name.")
@as_slash_command("command", "A command")
async def command(self, ctx: tanjun.abc.SlashContext, name: str) -> None:
...
Returns
- collections.abc.Callable[[_SlashCommandT], _SlashCommandT]: Decorator callback which adds the option to the command.
View Source
def with_int_slash_option( name: str, description: str, /, *, choices: typing.Optional[collections.Mapping[str, int]] = None, converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (), default: typing.Any = _UNDEFINED_DEFAULT, min_value: typing.Optional[int] = None, max_value: typing.Optional[int] = None, pass_as_kwarg: bool = True, ) -> collections.Callable[[_SlashCommandT], _SlashCommandT]: """Add an integer option to a slash command. For information on this function's parameters see `SlashCommand.add_int_option`. Examples -------- ```py @with_int_slash_option("int_value", "Int value.") @as_slash_command("command", "A command") async def command(self, ctx: tanjun.abc.SlashContext, int_value: int) -> None: ... ``` Returns ------- collections.abc.Callable[[_SlashCommandT], _SlashCommandT] Decorator callback which adds the option to the command. """ return lambda c: c.add_int_option( name, description, default=default, choices=choices, converters=converters, min_value=min_value, max_value=max_value, pass_as_kwarg=pass_as_kwarg, _stack_level=1, )
Add an integer option to a slash command.
For information on this function's parameters see SlashCommand.add_int_option.
Examples
@with_int_slash_option("int_value", "Int value.")
@as_slash_command("command", "A command")
async def command(self, ctx: tanjun.abc.SlashContext, int_value: int) -> None:
...
Returns
- collections.abc.Callable[[_SlashCommandT], _SlashCommandT]: Decorator callback which adds the option to the command.
View Source
def with_float_slash_option( name: str, description: str, /, *, always_float: bool = True, choices: typing.Optional[collections.Mapping[str, float]] = None, converters: typing.Union[collections.Collection[ConverterSig], ConverterSig] = (), default: typing.Any = _UNDEFINED_DEFAULT, min_value: typing.Optional[float] = None, max_value: typing.Optional[float] = None, pass_as_kwarg: bool = True, ) -> collections.Callable[[_SlashCommandT], _SlashCommandT]: """Add a float option to a slash command. For information on this function's parameters see `SlashCommand.add_float_option`. Examples -------- ```py @with_float_slash_option("float_value", "Float value.") @as_slash_command("command", "A command") async def command(self, ctx: tanjun.abc.SlashContext, float_value: float) -> None: ... ``` Returns ------- collections.abc.Callable[[_SlashCommandT], _SlashCommandT] Decorator callback which adds the option to the command. """ return lambda c: c.add_float_option( name, description, always_float=always_float, default=default, choices=choices, converters=converters, min_value=min_value, max_value=max_value, pass_as_kwarg=pass_as_kwarg, _stack_level=1, )
Add a float option to a slash command.
For information on this function's parameters see SlashCommand.add_float_option.
Examples
@with_float_slash_option("float_value", "Float value.")
@as_slash_command("command", "A command")
async def command(self, ctx: tanjun.abc.SlashContext, float_value: float) -> None:
...
Returns
- collections.abc.Callable[[_SlashCommandT], _SlashCommandT]: Decorator callback which adds the option to the command.
View Source
def with_bool_slash_option( name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True ) -> collections.Callable[[_SlashCommandT], _SlashCommandT]: """Add a boolean option to a slash command. For information on this function's parameters see `SlashContext.add_bool_option`. Examples -------- ```py @with_bool_slash_option("flag", "Whether this flag should be enabled.", default=False) @as_slash_command("command", "A command") async def command(self, ctx: tanjun.abc.SlashContext, flag: bool) -> None: ... ``` Returns ------- collections.abc.Callable[[_SlashCommandT], _SlashCommandT] Decorator callback which adds the option to the command. """ return lambda c: c.add_bool_option(name, description, default=default, pass_as_kwarg=pass_as_kwarg)
Add a boolean option to a slash command.
For information on this function's parameters see SlashContext.add_bool_option.
Examples
@with_bool_slash_option("flag", "Whether this flag should be enabled.", default=False)
@as_slash_command("command", "A command")
async def command(self, ctx: tanjun.abc.SlashContext, flag: bool) -> None:
...
Returns
- collections.abc.Callable[[_SlashCommandT], _SlashCommandT]: Decorator callback which adds the option to the command.
View Source
def with_role_slash_option( name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True ) -> collections.Callable[[_SlashCommandT], _SlashCommandT]: """Add a role option to a slash command. For information on this function's parameters see `SlashCommand.add_role_option`. Examples -------- ```py @with_role_slash_option("role", "Role to target.") @as_slash_command("command", "A command") async def command(self, ctx: tanjun.abc.SlashContext, role: hikari.Role) -> None: ... ``` Returns ------- collections.abc.Callable[[_SlashCommandT], _SlashCommandT] Decorator callback which adds the option to the command. """ return lambda c: c.add_role_option(name, description, default=default, pass_as_kwarg=pass_as_kwarg)
Add a role option to a slash command.
For information on this function's parameters see SlashCommand.add_role_option.
Examples
@with_role_slash_option("role", "Role to target.")
@as_slash_command("command", "A command")
async def command(self, ctx: tanjun.abc.SlashContext, role: hikari.Role) -> None:
...
Returns
- collections.abc.Callable[[_SlashCommandT], _SlashCommandT]: Decorator callback which adds the option to the command.
View Source
def with_user_slash_option( name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True ) -> collections.Callable[[_SlashCommandT], _SlashCommandT]: """Add a user option to a slash command. For information on this function's parameters see `SlashContext.add_user_option`. .. note:: This may result in `hikari.InteractionMember` or `hikari.users.User` if the user isn't in the current guild or if this command was executed in a DM channel. Examples -------- ```py @with_user_slash_option("user", "user to target.") @as_slash_command("command", "A command") async def command(self, ctx: tanjun.abc.SlashContext, user: Union[InteractionMember, User]) -> None: ... ``` Returns ------- collections.abc.Callable[[_SlashCommandT], _SlashCommandT] Decorator callback which adds the option to the command. """ return lambda c: c.add_user_option(name, description, default=default, pass_as_kwarg=pass_as_kwarg)
Add a user option to a slash command.
For information on this function's parameters see SlashContext.add_user_option.
Note:
This may result in hikari.InteractionMember or
hikari.users.User if the user isn't in the current guild or if this
command was executed in a DM channel.
Examples
@with_user_slash_option("user", "user to target.")
@as_slash_command("command", "A command")
async def command(self, ctx: tanjun.abc.SlashContext, user: Union[InteractionMember, User]) -> None:
...
Returns
- collections.abc.Callable[[_SlashCommandT], _SlashCommandT]: Decorator callback which adds the option to the command.
View Source
def with_member_slash_option( name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT ) -> collections.Callable[[_SlashCommandT], _SlashCommandT]: """Add a member option to a slash command. For information on this function's arguments see `SlashCommand.add_member_option`. .. note:: This will always result in `hikari.InteractionMember`. Examples -------- ```py @with_member_slash_option("member", "member to target.") @as_slash_command("command", "A command") async def command(self, ctx: tanjun.abc.SlashContext, member: hikari.InteractionMember) -> None: ... ``` Returns ------- collections.abc.Callable[[_SlashCommandT], _SlashCommandT] Decorator callback which adds the option to the command. """ return lambda c: c.add_member_option(name, description, default=default)
Add a member option to a slash command.
For information on this function's arguments see SlashCommand.add_member_option.
Note:
This will always result in hikari.InteractionMember.
Examples
@with_member_slash_option("member", "member to target.")
@as_slash_command("command", "A command")
async def command(self, ctx: tanjun.abc.SlashContext, member: hikari.InteractionMember) -> None:
...
Returns
- collections.abc.Callable[[_SlashCommandT], _SlashCommandT]: Decorator callback which adds the option to the command.
View Source
def with_channel_slash_option( name: str, description: str, /, *, types: typing.Union[collections.Collection[type[hikari.PartialChannel]], None] = None, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True, ) -> collections.Callable[[_SlashCommandT], _SlashCommandT]: """Add a channel option to a slash command. For information on this function's parameters see `SlashCommand.add_channel_option`. .. note:: This will always result in `hikari..InteractionChannel`. Examples -------- ```py @with_channel_slash_option("channel", "channel to target.") @as_slash_command("command", "A command") async def command(self, ctx: tanjun.abc.SlashContext, channel: hikari.InteractionChannel) -> None: ... ``` Returns ------- collections.abc.Callable[[_SlashCommandT], _SlashCommandT] Decorator callback which adds the option to the command. """ return lambda c: c.add_channel_option(name, description, types=types, default=default, pass_as_kwarg=pass_as_kwarg)
Add a channel option to a slash command.
For information on this function's parameters see SlashCommand.add_channel_option.
Note:
This will always result in hikari..InteractionChannel.
Examples
@with_channel_slash_option("channel", "channel to target.")
@as_slash_command("command", "A command")
async def command(self, ctx: tanjun.abc.SlashContext, channel: hikari.InteractionChannel) -> None:
...
Returns
- collections.abc.Callable[[_SlashCommandT], _SlashCommandT]: Decorator callback which adds the option to the command.
View Source
def with_mentionable_slash_option( name: str, description: str, /, *, default: typing.Any = _UNDEFINED_DEFAULT, pass_as_kwarg: bool = True ) -> collections.Callable[[_SlashCommandT], _SlashCommandT]: """Add a mentionable option to a slash command. For information on this function's arguments see `SlashCommand.add_mentionable_option`. .. note:: This may target roles, guild members or users and results in `Union[hikari.User, hikari.InteractionMember, hikari.Role]`. Examples -------- ```py @with_mentionable_slash_option("mentionable", "Mentionable entity to target.") @as_slash_command("command", "A command") async def command(self, ctx: tanjun.abc.SlashContext, mentionable: [Role, InteractionMember, User]) -> None: ... ``` Returns ------- collections.abc.Callable[[_SlashCommandT], _SlashCommandT] Decorator callback which adds the option to the command. """ return lambda c: c.add_mentionable_option(name, description, default=default, pass_as_kwarg=pass_as_kwarg)
Add a mentionable option to a slash command.
For information on this function's arguments see SlashCommand.add_mentionable_option.
Note:
This may target roles, guild members or users and results in
Union[hikari.User, hikari.InteractionMember, hikari.Role].
Examples
@with_mentionable_slash_option("mentionable", "Mentionable entity to target.")
@as_slash_command("command", "A command")
async def command(self, ctx: tanjun.abc.SlashContext, mentionable: [Role, InteractionMember, User]) -> None:
...
Returns
- collections.abc.Callable[[_SlashCommandT], _SlashCommandT]: Decorator callback which adds the option to the command.
View Source
# -*- coding: utf-8 -*- # cython: language_level=3 # BSD 3-Clause License # # Copyright (c) 2020-2022, Faster Speeding # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Standard implementation of Tanjun's "components" used to manage separate features within a client.""" from __future__ import annotations __all__: list[str] = [ "CommandT", "Component", "AbstractComponentLoader", "OnCallbackSig", "OnCallbackSigT", "WithCommandReturnSig", ] import abc import asyncio import base64 import copy import inspect import itertools import logging import random import typing from collections import abc as collections from . import abc as tanjun_abc from . import checks as checks_ from . import errors from . import injecting from . import utilities if typing.TYPE_CHECKING: from hikari.events import base_events from . import schedules _ComponentT = typing.TypeVar("_ComponentT", bound="Component") _ScheduleT = typing.TypeVar("_ScheduleT", bound=schedules.AbstractSchedule) CommandT = typing.TypeVar("CommandT", bound="tanjun_abc.ExecutableCommand[typing.Any]") _LOGGER = logging.getLogger("hikari.tanjun.components") # This errors on earlier 3.9 releases when not quotes cause dumb handling of the [CommandT] list WithCommandReturnSig = typing.Union[CommandT, "collections.Callable[[CommandT], CommandT]"] OnCallbackSig = collections.Callable[..., tanjun_abc.MaybeAwaitableT[None]] """Type hint of a on_open or on_close component callback. These support dependency injection, should expect no positional arguments and should return `None`. """ OnCallbackSigT = typing.TypeVar("OnCallbackSigT", bound=OnCallbackSig) """Generic version of `OnCallbackSig`.""" class AbstractComponentLoader(abc.ABC): """Abstract interface used for loading utility into a standard `Component`.""" __slots__ = () @abc.abstractmethod def load_into_component(self, component: tanjun_abc.Component, /) -> None: """Load the object into the component. Parameters ---------- component : tanjun.abc.Component The component this object should be loaded into. """ def _with_command( add_command: collections.Callable[[CommandT], Component], maybe_command: typing.Optional[CommandT], /, *, copy: bool = False, ) -> WithCommandReturnSig[CommandT]: if maybe_command: maybe_command = maybe_command.copy() if copy else maybe_command add_command(maybe_command) return maybe_command def decorator(command: CommandT, /) -> CommandT: command = command.copy() if copy else command add_command(command) return command return decorator def _filter_scope(scope: collections.Mapping[str, typing.Any]) -> collections.Iterator[typing.Any]: return (value for key, value in scope.items() if not key.startswith("_")) class _ComponentManager(tanjun_abc.ClientLoader): __slots__ = ("_component", "_copy") def __init__(self, component: Component, copy: bool) -> None: self._component = component self._copy = copy @property def has_load(self) -> bool: return True @property def has_unload(self) -> bool: return True def load(self, client: tanjun_abc.Client, /) -> bool: client.add_component(self._component.copy() if self._copy else self._component) return True def unload(self, client: tanjun_abc.Client, /) -> bool: client.remove_component_by_name(self._component.name) return True # TODO: do we want to setup a custom equality and hash here to make it easier to unload components? class Component(tanjun_abc.Component): """Standard implementation of `tanjun.abc.Component`. This is a collcetion of commands (both message and slash), hooks and listener callbacks which can be added to a generic client. .. note:: This implementation supports dependency injection for its checks, command callbacks and listeners when linked to a client which supports dependency injection. """ __slots__ = ( "_checks", "_client", "_client_callbacks", "_defaults_to_ephemeral", "_hooks", "_is_strict", "_listeners", "_loop", "_message_commands", "_message_hooks", "_metadata", "_name", "_names_to_commands", "_on_close", "_on_open", "_schedules", "_slash_commands", "_slash_hooks", ) def __init__(self, *, name: typing.Optional[str] = None, strict: bool = False) -> None: """Initialise a new component. Other Parameters ---------------- name : str The component's identifier. If not provided then this will be a random string. strict : bool Whether this component should use a stricter (more optimal) approach for message command search. When this is `True`, message command names will not be allowed to contain spaces and will have to be unique to one command within the component. """ self._checks: list[checks_.InjectableCheck] = [] self._client: typing.Optional[tanjun_abc.Client] = None self._client_callbacks: dict[str, list[tanjun_abc.MetaEventSig]] = {} self._defaults_to_ephemeral: typing.Optional[bool] = None self._hooks: typing.Optional[tanjun_abc.AnyHooks] = None self._is_strict = strict self._listeners: dict[type[base_events.Event], list[tanjun_abc.ListenerCallbackSig]] = {} self._loop: typing.Optional[asyncio.AbstractEventLoop] = None self._message_commands: list[tanjun_abc.MessageCommand[typing.Any]] = [] self._message_hooks: typing.Optional[tanjun_abc.MessageHooks] = None self._metadata: dict[typing.Any, typing.Any] = {} self._name = name or base64.b64encode(random.randbytes(32)).decode() self._names_to_commands: dict[str, tanjun_abc.MessageCommand[typing.Any]] = {} self._on_close: list[injecting.CallbackDescriptor[None]] = [] self._on_open: list[injecting.CallbackDescriptor[None]] = [] self._schedules: list[schedules.AbstractSchedule] = [] self._slash_commands: dict[str, tanjun_abc.BaseSlashCommand] = {} self._slash_hooks: typing.Optional[tanjun_abc.SlashHooks] = None def __repr__(self) -> str: return f"{type(self).__name__}({self.checks=}, {self.hooks=}, {self.slash_hooks=}, {self.message_hooks=})" @property def checks(self) -> collections.Collection[tanjun_abc.CheckSig]: """Collection of the checks being run against every command execution in this component.""" return tuple(check.callback for check in self._checks) @property def client(self) -> typing.Optional[tanjun_abc.Client]: # <<inherited docstring from tanjun.abc.Component>>. return self._client @property def defaults_to_ephemeral(self) -> typing.Optional[bool]: # <<inherited docstring from tanjun.abc.Component>>. return self._defaults_to_ephemeral @property def hooks(self) -> typing.Optional[tanjun_abc.AnyHooks]: # <<inherited docstring from tanjun.abc.Component>>. return self._hooks @property def loop(self) -> typing.Optional[asyncio.AbstractEventLoop]: # <<inherited docstring from tanjun.abc.Component>>. return self._loop @property def name(self) -> str: # <<inherited docstring from tanjun.abc.Component>>. return self._name @property def schedules(self) -> collections.Collection[schedules.AbstractSchedule]: """Collection of the schedules registered to this component.""" return self._schedules @property def slash_commands(self) -> collections.Collection[tanjun_abc.BaseSlashCommand]: # <<inherited docstring from tanjun.abc.Component>>. return self._slash_commands.copy().values() @property def slash_hooks(self) -> typing.Optional[tanjun_abc.SlashHooks]: # <<inherited docstring from tanjun.abc.Component>>. return self._slash_hooks @property def message_commands(self) -> collections.Collection[tanjun_abc.MessageCommand[typing.Any]]: # <<inherited docstring from tanjun.abc.Component>>. return self._message_commands.copy() @property def message_hooks(self) -> typing.Optional[tanjun_abc.MessageHooks]: # <<inherited docstring from tanjun.abc.Component>>. return self._message_hooks @property def needs_injector(self) -> bool: """Whether any of the checks in this component require dependency injection.""" return any(check.needs_injector for check in self._checks) @property def listeners( self, ) -> collections.Mapping[type[base_events.Event], collections.Collection[tanjun_abc.ListenerCallbackSig]]: # <<inherited docstring from tanjun.abc.Component>>. return utilities.CastedView(self._listeners, lambda x: x.copy()) @property def metadata(self) -> dict[typing.Any, typing.Any]: # <<inherited docstring from tanjun.abc.Component>>. return self._metadata def copy(self: _ComponentT, *, _new: bool = True) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. if not _new: self._checks = [check.copy() for check in self._checks] self._slash_commands = {name: command.copy() for name, command in self._slash_commands.items()} self._hooks = self._hooks.copy() if self._hooks else None self._listeners = { event: [copy.copy(listener) for listener in listeners] for event, listeners in self._listeners.items() } commands = {command: command.copy() for command in self._message_commands} self._message_commands = list(commands.values()) self._metadata = self._metadata.copy() self._names_to_commands = {name: commands[command] for name, command in self._names_to_commands.items()} self._schedules = [schedule.copy() for schedule in self._schedules] if self._schedules else [] return self return copy.copy(self).copy(_new=False) @typing.overload def load_from_scope( self: _ComponentT, *, scope: typing.Optional[collections.Mapping[str, typing.Any]] = None ) -> _ComponentT: ... @typing.overload def load_from_scope(self: _ComponentT, *, include_globals: bool = False) -> _ComponentT: ... def load_from_scope( self: _ComponentT, *, include_globals: bool = False, scope: typing.Optional[collections.Mapping[str, typing.Any]] = None, ) -> _ComponentT: """Load entries such as top-level commands into the component from the calling scope. Notes ----- * This will load schedules which support `AbstractComponentLoader` (e.g. `tanjun.schedules.IntervalSchedule`). * This will ignore commands which are owned by command groups. * This will detect entries from the calling scope which implement `AbstractComponentLoader` unless `scope` is passed but this isn't possible in a stack-less python implementation; in stack-less environments the scope will have to be explicitly passed as `scope`. Other Parameters ---------------- include_globals: bool Whether to include global variables (along with local) while detecting from the calling scope. This defaults to `False`, cannot be `True` when `scope` is provided and will only ever be needed when the local scope is different from the global scope. scope : typing.Optional[collections.Mapping[str, typing.Any]] The scope to detect entries which implement `AbstractComponentLoader` from. This overrides the default usage of stackframe introspection. Returns ------- Self The current component to allow for chaining. Raises ------ RuntimeError If this is called in a python implementation which doesn't support stack frame inspection when `scope` is not provided. ValueError If `scope` is provided when `include_globals` is True. """ if scope is None: if not (stack := inspect.currentframe()) or not stack.f_back: raise RuntimeError( "Stackframe introspection is not supported in this runtime. Please explicitly pass `scope`." ) values_iter = _filter_scope(stack.f_back.f_locals) if include_globals: values_iter = itertools.chain(values_iter, _filter_scope(stack.f_back.f_globals)) elif include_globals: raise ValueError("Cannot specify include_globals as True when scope is passed") else: values_iter = _filter_scope(scope) _LOGGER.info( "Loading commands for %s component from %s parent scope(s)", self.name, "global and local" if include_globals else "local", ) for value in values_iter: if isinstance(value, AbstractComponentLoader): value.load_into_component(self) return self def set_ephemeral_default(self: _ComponentT, state: typing.Optional[bool], /) -> _ComponentT: """Set whether slash contexts executed in this component should default to ephemeral responses. Parameters ---------- typing.Optional[bool] Whether slash command contexts executed in this component should should default to ephemeral. This will be overridden by any response calls which specify flags. Setting this to `None` will let the default set on the parent client propagate and decide the ephemeral default behaviour. Returns ------- SelfT This component to enable method chaining. """ self._defaults_to_ephemeral = state return self def set_metadata(self: _ComponentT, key: typing.Any, value: typing.Any, /) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. self._metadata[key] = value return self def set_slash_hooks(self: _ComponentT, hooks_: typing.Optional[tanjun_abc.SlashHooks], /) -> _ComponentT: self._slash_hooks = hooks_ return self def set_message_hooks(self: _ComponentT, hooks_: typing.Optional[tanjun_abc.MessageHooks], /) -> _ComponentT: self._message_hooks = hooks_ return self def set_hooks(self: _ComponentT, hooks: typing.Optional[tanjun_abc.AnyHooks], /) -> _ComponentT: self._hooks = hooks return self def add_check(self: _ComponentT, check: tanjun_abc.CheckSig, /) -> _ComponentT: if check not in self._checks: self._checks.append(checks_.InjectableCheck(check)) return self def remove_check(self: _ComponentT, check: tanjun_abc.CheckSig, /) -> _ComponentT: self._checks.remove(typing.cast("checks_.InjectableCheck", check)) return self def with_check(self, check: tanjun_abc.CheckSigT, /) -> tanjun_abc.CheckSigT: self.add_check(check) return check def add_client_callback( self: _ComponentT, event_name: typing.Union[str, tanjun_abc.ClientCallbackNames], callback: tanjun_abc.MetaEventSig, /, ) -> _ComponentT: event_name = event_name.lower() try: if callback in self._client_callbacks[event_name]: return self self._client_callbacks[event_name].append(callback) except KeyError: self._client_callbacks[event_name] = [callback] if self._client: self._client.add_client_callback(event_name, callback) return self def get_client_callbacks( self, event_name: typing.Union[str, tanjun_abc.ClientCallbackNames], / ) -> collections.Collection[tanjun_abc.MetaEventSig]: event_name = event_name.lower() return self._client_callbacks.get(event_name) or () def remove_client_callback(self, event_name: str, callback: tanjun_abc.MetaEventSig, /) -> None: event_name = event_name.lower() self._client_callbacks[event_name].remove(callback) if not self._client_callbacks[event_name]: del self._client_callbacks[event_name] if self._client: self._client.remove_client_callback(event_name, callback) def with_client_callback( self, event_name: typing.Union[str, tanjun_abc.ClientCallbackNames], / ) -> collections.Callable[[tanjun_abc.MetaEventSigT], tanjun_abc.MetaEventSigT]: def decorator(callback: tanjun_abc.MetaEventSigT, /) -> tanjun_abc.MetaEventSigT: self.add_client_callback(event_name, callback) return callback return decorator def add_command(self: _ComponentT, command: tanjun_abc.ExecutableCommand[typing.Any], /) -> _ComponentT: """Add a command to this component. Parameters ---------- command : tanjun.abc.ExecutableCommand[typing.Any] The command to add. Returns ------- Self The current component to allow for chaining. """ if isinstance(command, tanjun_abc.MessageCommand): self.add_message_command(command) elif isinstance(command, tanjun_abc.BaseSlashCommand): self.add_slash_command(command) else: raise ValueError( f"Unexpected object passed, expected a MessageCommand or BaseSlashCommand but got {type(command)}" ) return self def remove_command(self: _ComponentT, command: tanjun_abc.ExecutableCommand[typing.Any], /) -> _ComponentT: """Remove a command from this component. Parameters ---------- command : tanjun.abc.ExecutableCommand[typing.Any] The command to remove. Returns ------- Self This component to enable method chaining. """ if isinstance(command, tanjun_abc.MessageCommand): self.remove_message_command(command) elif isinstance(command, tanjun_abc.BaseSlashCommand): self.remove_slash_command(command) else: raise ValueError( f"Unexpected object passed, expected a MessageCommand or BaseSlashCommand but got {type(command)}" ) return self @typing.overload def with_command(self, command: CommandT, /) -> CommandT: ... @typing.overload def with_command(self, /, *, copy: bool = False) -> collections.Callable[[CommandT], CommandT]: ... def with_command( self, command: typing.Optional[CommandT] = None, /, *, copy: bool = False ) -> WithCommandReturnSig[CommandT]: """Add a command to this component through a decorator call. Examples -------- This may be used inconjunction with `tanjun.as_slash_command` and `tanjun.as_message_command`. ```py @component.with_command @tanjun.with_slash_str_option("option_name", "option description") @tanjun.as_slash_command("command_name", "command description") async def slash_command(ctx: tanjun.abc.Context, arg: str) -> None: await ctx.respond(f"Hi {arg}") ``` ```py @component.with_command @tanjun.with_argument("argument_name") @tanjun.as_message_command("command_name") async def message_command(ctx: tanjun.abc.Context, arg: str) -> None: await ctx.respond(f"Hi {arg}") ``` Parameters ---------- command CommandT The command to add to this component. Other Parameters ---------------- copy : bool Whether to copy the command before adding it to this component. Returns ------- CommandT The added command. """ return _with_command(self.add_command, command, copy=copy) def add_slash_command(self: _ComponentT, command: tanjun_abc.BaseSlashCommand, /) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. if self._slash_commands.get(command.name) == command: return self command.bind_component(self) if self._client: command.bind_client(self._client) self._slash_commands[command.name.casefold()] = command return self def remove_slash_command(self: _ComponentT, command: tanjun_abc.BaseSlashCommand, /) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. try: del self._slash_commands[command.name.casefold()] except KeyError: raise ValueError(f"Command {command.name} not found") from None return self @typing.overload def with_slash_command(self, command: tanjun_abc.BaseSlashCommandT, /) -> tanjun_abc.BaseSlashCommandT: ... @typing.overload def with_slash_command( self, /, *, copy: bool = False ) -> collections.Callable[[tanjun_abc.BaseSlashCommandT], tanjun_abc.BaseSlashCommandT]: ... def with_slash_command( self, command: typing.Optional[tanjun_abc.BaseSlashCommandT] = None, /, *, copy: bool = False ) -> WithCommandReturnSig[tanjun_abc.BaseSlashCommandT]: # <<inherited docstring from tanjun.abc.Component>>. return _with_command(self.add_slash_command, command, copy=copy) def add_message_command(self: _ComponentT, command: tanjun_abc.MessageCommand[typing.Any], /) -> _ComponentT: """Add a message command to the component. Parameters ---------- command : tanjun.abc.MessageCommand The command to add. Returns ------- Self The component to allow method chaining. Raises ------ ValueError If one of the command's name is already registered in a strict component. """ if command in self._message_commands: return self if self._is_strict: if any(" " in name for name in command.names): raise ValueError("Command name cannot contain spaces for this component implementation") if name_conflicts := self._names_to_commands.keys() & command.names: raise ValueError( "Sub-command names must be unique in a strict component. " "The following conflicts were found " + ", ".join(name_conflicts) ) self._names_to_commands.update((name, command) for name in command.names) self._message_commands.append(command) if self._client: command.bind_client(self._client) command.bind_component(self) return self def remove_message_command(self: _ComponentT, command: tanjun_abc.MessageCommand[typing.Any], /) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. self._message_commands.remove(command) if self._is_strict: for name in command.names: if self._names_to_commands.get(name) == command: del self._names_to_commands[name] return self @typing.overload def with_message_command(self, command: tanjun_abc.MessageCommandT, /) -> tanjun_abc.MessageCommandT: ... @typing.overload def with_message_command( self, /, *, copy: bool = False ) -> collections.Callable[[tanjun_abc.MessageCommandT], tanjun_abc.MessageCommandT]: ... def with_message_command( self, command: typing.Optional[tanjun_abc.MessageCommandT] = None, /, *, copy: bool = False ) -> WithCommandReturnSig[tanjun_abc.MessageCommandT]: # <<inherited docstring from tanjun.abc.Component>>. return _with_command(self.add_message_command, command, copy=copy) def add_listener( self: _ComponentT, event: type[base_events.Event], listener: tanjun_abc.ListenerCallbackSig, / ) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. try: if listener in self._listeners[event]: return self self._listeners[event].append(listener) except KeyError: self._listeners[event] = [listener] if self._client: self._client.add_listener(event, listener) return self def remove_listener( self: _ComponentT, event: type[base_events.Event], listener: tanjun_abc.ListenerCallbackSig, / ) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. self._listeners[event].remove(listener) if not self._listeners[event]: del self._listeners[event] if self._client: self._client.remove_listener(event, listener) return self # TODO: make event optional? def with_listener( self, event_type: type[base_events.Event] ) -> collections.Callable[[tanjun_abc.ListenerCallbackSigT], tanjun_abc.ListenerCallbackSigT]: # <<inherited docstring from tanjun.abc.Component>>. def decorator(callback: tanjun_abc.ListenerCallbackSigT) -> tanjun_abc.ListenerCallbackSigT: self.add_listener(event_type, callback) return callback return decorator def add_on_close(self: _ComponentT, callback: OnCallbackSig, /) -> _ComponentT: """Add a close callback to this component. .. note:: Unlike the closing and closed client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client. Parameters ---------- callback : OnCallbackSig The close callback to add to this component. This should take no positional arguments, return `None` and may take use injected dependencies. Returns ------- Self The component object to enable call chaining. """ self._on_close.append(injecting.CallbackDescriptor(callback)) return self def with_on_close(self, callback: OnCallbackSigT, /) -> OnCallbackSigT: """Add a close callback to this component through a decorator call. .. note:: Unlike the closing and closed client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client. Parameters ---------- callback : OnCallbackSig The close callback to add to this component. This should take no positional arguments, return `None` and may take use injected dependencies. Returns ------- OnCallbackSig The added close callback. """ self.add_on_close(callback) return callback def add_on_open(self: _ComponentT, callback: OnCallbackSig, /) -> _ComponentT: """Add a open callback to this component. .. note:: Unlike the starting and started client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client. Parameters ---------- callback : OnCallbackSig The open callback to add to this component. This should take no positional arguments, return `None` and may take use injected dependencies. Returns ------- Self The component object to enable call chaining. """ self._on_open.append(injecting.CallbackDescriptor(callback)) return self def with_on_open(self, callback: OnCallbackSigT, /) -> OnCallbackSigT: """Add a open callback to this component through a decorator call. .. note:: Unlike the starting and started client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client. Parameters ---------- callback : OnCallbackSig The open callback to add to this component. This should take no positional arguments, return `None` and may take use injected dependencies. Returns ------- OnCallbackSig The added open callback. """ self.add_on_open(callback) return callback def bind_client(self: _ComponentT, client: tanjun_abc.Client, /) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. if self._client: raise RuntimeError("Client already set") self._client = client for message_command in self._message_commands: message_command.bind_client(client) for slash_command in self._slash_commands.values(): slash_command.bind_client(client) for event, listeners in self._listeners.items(): for listener in listeners: self._client.add_listener(event, listener) for event_name, callbacks in self._client_callbacks.items(): for callback in callbacks: self._client.add_client_callback(event_name, callback) return self def unbind_client(self: _ComponentT, client: tanjun_abc.Client, /) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. if not self._client or self._client != client: raise RuntimeError("Component isn't bound to this client") for event, listeners in self._listeners.items(): for listener in listeners: try: self._client.remove_listener(event, listener) except (LookupError, ValueError): pass for event_name, callbacks in self._client_callbacks.items(): for callback in callbacks: try: self._client.remove_client_callback(event_name, callback) except (LookupError, ValueError): pass self._client = None return self async def _check_context(self, ctx: tanjun_abc.Context, /) -> bool: return await utilities.gather_checks(ctx, self._checks) async def _check_message_context( self, ctx: tanjun_abc.MessageContext, / ) -> collections.AsyncIterator[tuple[str, tanjun_abc.MessageCommand[typing.Any]]]: ctx.set_component(self) if self._is_strict: name = ctx.content.split(" ", 1)[0] command = self._names_to_commands.get(name) if command and await self._check_context(ctx) and await command.check_context(ctx): yield name, command else: ctx.set_component(None) return checks_run = False for name, command in self.check_message_name(ctx.content): if not checks_run: if not await self._check_context(ctx): return checks_run = True if await command.check_context(ctx): yield name, command ctx.set_component(None) def check_message_name( self, content: str, / ) -> collections.Iterator[tuple[str, tanjun_abc.MessageCommand[typing.Any]]]: # <<inherited docstring from tanjun.abc.Component>>. if self._is_strict: name = content.split(" ", 1)[0] if command := self._names_to_commands.get(name): yield name, command return for command in self._message_commands: if (name_ := utilities.match_prefix_names(content, command.names)) is not None: yield name_, command def check_slash_name(self, name: str, /) -> collections.Iterator[tanjun_abc.BaseSlashCommand]: # <<inherited docstring from tanjun.abc.Component>>. if command := self._slash_commands.get(name): yield command async def _execute_interaction( self, ctx: tanjun_abc.SlashContext, command: typing.Optional[tanjun_abc.BaseSlashCommand], /, *, hooks: typing.Optional[collections.MutableSet[tanjun_abc.SlashHooks]] = None, ) -> typing.Optional[collections.Awaitable[None]]: try: if not command or not await self._check_context(ctx) or not await command.check_context(ctx): return None except errors.HaltExecution: return asyncio.get_running_loop().create_task(ctx.mark_not_found()) except errors.CommandError as exc: await ctx.respond(exc.message) asyncio.get_running_loop().create_future().set_result(None) return None if self._slash_hooks: if hooks is None: hooks = set() hooks.add(self._slash_hooks) if self._hooks: if hooks is None: hooks = set() hooks.add(self._hooks) return asyncio.get_running_loop().create_task(command.execute(ctx, hooks=hooks)) # To ensure that ctx.set_ephemeral_default is called as soon as possible if # a match is found the public function is kept sync to avoid yielding # to the event loop until after this is set. def execute_interaction( self, ctx: tanjun_abc.SlashContext, /, *, hooks: typing.Optional[collections.MutableSet[tanjun_abc.SlashHooks]] = None, ) -> collections.Coroutine[typing.Any, typing.Any, typing.Optional[collections.Awaitable[None]]]: # <<inherited docstring from tanjun.abc.Component>>. command = self._slash_commands.get(ctx.interaction.command_name) if command: if command.defaults_to_ephemeral is not None: ctx.set_ephemeral_default(command.defaults_to_ephemeral) elif self._defaults_to_ephemeral is not None: ctx.set_ephemeral_default(self._defaults_to_ephemeral) return self._execute_interaction(ctx, command, hooks=hooks) async def execute_message( self, ctx: tanjun_abc.MessageContext, /, *, hooks: typing.Optional[collections.MutableSet[tanjun_abc.MessageHooks]] = None, ) -> bool: # <<inherited docstring from tanjun.abc.Component>>. async for name, command in self._check_message_context(ctx): ctx.set_triggering_name(name) ctx.set_content(ctx.content[len(name) :].lstrip()) ctx.set_component(self) # Only add our hooks if we're sure we'll be executing the command here. if self._message_hooks: if hooks is None: hooks = set() hooks.add(self._message_hooks) if self._hooks: if hooks is None: hooks = set() hooks.add(self._hooks) await command.execute(ctx, hooks=hooks) return True ctx.set_component(None) return False def _load_from_properties(self) -> None: for _, member in inspect.getmembers(self): if isinstance(member, AbstractComponentLoader): member.load_into_component(self) def add_schedule(self: _ComponentT, schedule: schedules.AbstractSchedule, /) -> _ComponentT: """Add a schedule to the component. Parameters ---------- schedule : tanjun.schedules.AbstractSchedule The schedule to add. Returns ------- Self The component itself for chaining. """ if self._client and self._loop: # TODO: upgrade this to the standard interface assert isinstance(self._client, injecting.InjectorClient) schedule.start(self._client, loop=self._loop) self._schedules.append(schedule) return self def remove_schedule(self: _ComponentT, schedule: schedules.AbstractSchedule, /) -> _ComponentT: """Remove a schedule from the component. Parameters ---------- schedule : tanjun.schedules.AbstractSchedule The schedule to remove Returns ------- Self The component itself for chaining. Raises ------ ValueError If the schedule isn't registered. """ if schedule.is_alive: schedule.stop() self._schedules.remove(schedule) return self def with_schedule(self, schedule: _ScheduleT, /) -> _ScheduleT: """Add a schedule to the component through a decorator call. Example ------- This may be used in conjunction with `tanjun.as_interval`. ```py @component.with_schedule @tanjun.as_interval(60) async def my_schedule(): print("I'm running every minute!") ``` Parameters ---------- schedule : schedules.AbstractSchedule The schedule to add. Returns ------- schedules.AbstractSchedule The added schedule. """ self.add_schedule(schedule) return schedule async def close(self, *, unbind: bool = False) -> None: # <<inherited docstring from tanjun.abc.Component>>. if not self._loop: raise RuntimeError("Component isn't active") assert self._client for schedule in self._schedules: if schedule.is_alive: schedule.stop() self._loop = None # TODO: upgrade this to the standard interface assert isinstance(self._client, injecting.InjectorClient) await asyncio.gather( *(callback.resolve(injecting.BasicInjectionContext(self._client)) for callback in self._on_close) ) if unbind: self.unbind_client(self._client) async def open(self) -> None: # <<inherited docstring from tanjun.abc.Component>>. if self._loop: raise RuntimeError("Component is already active") if not self._client: raise RuntimeError("Client isn't bound yet") self._loop = asyncio.get_running_loop() # TODO: upgrade this to the standard interface assert isinstance(self._client, injecting.InjectorClient) await asyncio.gather( *(callback.resolve(injecting.BasicInjectionContext(self._client)) for callback in self._on_open) ) for schedule in self._schedules: schedule.start(self._client, loop=self._loop) def make_loader(self, *, copy: bool = True) -> tanjun_abc.ClientLoader: """Make a loader/unloader for this component. This enables loading, unloading and reloading of this component into a client by targeting the module using `tanjun.Client.load_modules`, `tanjun.Client.unload_modules` and `tanjun.Client.reload_modules`. Other Parameters ---------------- copy: bool Whether to copy the component before loading it into a client. Defaults to `True`. Returns ------- tanjun.abc.ClientLoader The loader for this component. """ return _ComponentManager(self, copy)
Standard implementation of Tanjun's "components" used to manage separate features within a client.
View Source
class Component(tanjun_abc.Component): """Standard implementation of `tanjun.abc.Component`. This is a collcetion of commands (both message and slash), hooks and listener callbacks which can be added to a generic client. .. note:: This implementation supports dependency injection for its checks, command callbacks and listeners when linked to a client which supports dependency injection. """ __slots__ = ( "_checks", "_client", "_client_callbacks", "_defaults_to_ephemeral", "_hooks", "_is_strict", "_listeners", "_loop", "_message_commands", "_message_hooks", "_metadata", "_name", "_names_to_commands", "_on_close", "_on_open", "_schedules", "_slash_commands", "_slash_hooks", ) def __init__(self, *, name: typing.Optional[str] = None, strict: bool = False) -> None: """Initialise a new component. Other Parameters ---------------- name : str The component's identifier. If not provided then this will be a random string. strict : bool Whether this component should use a stricter (more optimal) approach for message command search. When this is `True`, message command names will not be allowed to contain spaces and will have to be unique to one command within the component. """ self._checks: list[checks_.InjectableCheck] = [] self._client: typing.Optional[tanjun_abc.Client] = None self._client_callbacks: dict[str, list[tanjun_abc.MetaEventSig]] = {} self._defaults_to_ephemeral: typing.Optional[bool] = None self._hooks: typing.Optional[tanjun_abc.AnyHooks] = None self._is_strict = strict self._listeners: dict[type[base_events.Event], list[tanjun_abc.ListenerCallbackSig]] = {} self._loop: typing.Optional[asyncio.AbstractEventLoop] = None self._message_commands: list[tanjun_abc.MessageCommand[typing.Any]] = [] self._message_hooks: typing.Optional[tanjun_abc.MessageHooks] = None self._metadata: dict[typing.Any, typing.Any] = {} self._name = name or base64.b64encode(random.randbytes(32)).decode() self._names_to_commands: dict[str, tanjun_abc.MessageCommand[typing.Any]] = {} self._on_close: list[injecting.CallbackDescriptor[None]] = [] self._on_open: list[injecting.CallbackDescriptor[None]] = [] self._schedules: list[schedules.AbstractSchedule] = [] self._slash_commands: dict[str, tanjun_abc.BaseSlashCommand] = {} self._slash_hooks: typing.Optional[tanjun_abc.SlashHooks] = None def __repr__(self) -> str: return f"{type(self).__name__}({self.checks=}, {self.hooks=}, {self.slash_hooks=}, {self.message_hooks=})" @property def checks(self) -> collections.Collection[tanjun_abc.CheckSig]: """Collection of the checks being run against every command execution in this component.""" return tuple(check.callback for check in self._checks) @property def client(self) -> typing.Optional[tanjun_abc.Client]: # <<inherited docstring from tanjun.abc.Component>>. return self._client @property def defaults_to_ephemeral(self) -> typing.Optional[bool]: # <<inherited docstring from tanjun.abc.Component>>. return self._defaults_to_ephemeral @property def hooks(self) -> typing.Optional[tanjun_abc.AnyHooks]: # <<inherited docstring from tanjun.abc.Component>>. return self._hooks @property def loop(self) -> typing.Optional[asyncio.AbstractEventLoop]: # <<inherited docstring from tanjun.abc.Component>>. return self._loop @property def name(self) -> str: # <<inherited docstring from tanjun.abc.Component>>. return self._name @property def schedules(self) -> collections.Collection[schedules.AbstractSchedule]: """Collection of the schedules registered to this component.""" return self._schedules @property def slash_commands(self) -> collections.Collection[tanjun_abc.BaseSlashCommand]: # <<inherited docstring from tanjun.abc.Component>>. return self._slash_commands.copy().values() @property def slash_hooks(self) -> typing.Optional[tanjun_abc.SlashHooks]: # <<inherited docstring from tanjun.abc.Component>>. return self._slash_hooks @property def message_commands(self) -> collections.Collection[tanjun_abc.MessageCommand[typing.Any]]: # <<inherited docstring from tanjun.abc.Component>>. return self._message_commands.copy() @property def message_hooks(self) -> typing.Optional[tanjun_abc.MessageHooks]: # <<inherited docstring from tanjun.abc.Component>>. return self._message_hooks @property def needs_injector(self) -> bool: """Whether any of the checks in this component require dependency injection.""" return any(check.needs_injector for check in self._checks) @property def listeners( self, ) -> collections.Mapping[type[base_events.Event], collections.Collection[tanjun_abc.ListenerCallbackSig]]: # <<inherited docstring from tanjun.abc.Component>>. return utilities.CastedView(self._listeners, lambda x: x.copy()) @property def metadata(self) -> dict[typing.Any, typing.Any]: # <<inherited docstring from tanjun.abc.Component>>. return self._metadata def copy(self: _ComponentT, *, _new: bool = True) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. if not _new: self._checks = [check.copy() for check in self._checks] self._slash_commands = {name: command.copy() for name, command in self._slash_commands.items()} self._hooks = self._hooks.copy() if self._hooks else None self._listeners = { event: [copy.copy(listener) for listener in listeners] for event, listeners in self._listeners.items() } commands = {command: command.copy() for command in self._message_commands} self._message_commands = list(commands.values()) self._metadata = self._metadata.copy() self._names_to_commands = {name: commands[command] for name, command in self._names_to_commands.items()} self._schedules = [schedule.copy() for schedule in self._schedules] if self._schedules else [] return self return copy.copy(self).copy(_new=False) @typing.overload def load_from_scope( self: _ComponentT, *, scope: typing.Optional[collections.Mapping[str, typing.Any]] = None ) -> _ComponentT: ... @typing.overload def load_from_scope(self: _ComponentT, *, include_globals: bool = False) -> _ComponentT: ... def load_from_scope( self: _ComponentT, *, include_globals: bool = False, scope: typing.Optional[collections.Mapping[str, typing.Any]] = None, ) -> _ComponentT: """Load entries such as top-level commands into the component from the calling scope. Notes ----- * This will load schedules which support `AbstractComponentLoader` (e.g. `tanjun.schedules.IntervalSchedule`). * This will ignore commands which are owned by command groups. * This will detect entries from the calling scope which implement `AbstractComponentLoader` unless `scope` is passed but this isn't possible in a stack-less python implementation; in stack-less environments the scope will have to be explicitly passed as `scope`. Other Parameters ---------------- include_globals: bool Whether to include global variables (along with local) while detecting from the calling scope. This defaults to `False`, cannot be `True` when `scope` is provided and will only ever be needed when the local scope is different from the global scope. scope : typing.Optional[collections.Mapping[str, typing.Any]] The scope to detect entries which implement `AbstractComponentLoader` from. This overrides the default usage of stackframe introspection. Returns ------- Self The current component to allow for chaining. Raises ------ RuntimeError If this is called in a python implementation which doesn't support stack frame inspection when `scope` is not provided. ValueError If `scope` is provided when `include_globals` is True. """ if scope is None: if not (stack := inspect.currentframe()) or not stack.f_back: raise RuntimeError( "Stackframe introspection is not supported in this runtime. Please explicitly pass `scope`." ) values_iter = _filter_scope(stack.f_back.f_locals) if include_globals: values_iter = itertools.chain(values_iter, _filter_scope(stack.f_back.f_globals)) elif include_globals: raise ValueError("Cannot specify include_globals as True when scope is passed") else: values_iter = _filter_scope(scope) _LOGGER.info( "Loading commands for %s component from %s parent scope(s)", self.name, "global and local" if include_globals else "local", ) for value in values_iter: if isinstance(value, AbstractComponentLoader): value.load_into_component(self) return self def set_ephemeral_default(self: _ComponentT, state: typing.Optional[bool], /) -> _ComponentT: """Set whether slash contexts executed in this component should default to ephemeral responses. Parameters ---------- typing.Optional[bool] Whether slash command contexts executed in this component should should default to ephemeral. This will be overridden by any response calls which specify flags. Setting this to `None` will let the default set on the parent client propagate and decide the ephemeral default behaviour. Returns ------- SelfT This component to enable method chaining. """ self._defaults_to_ephemeral = state return self def set_metadata(self: _ComponentT, key: typing.Any, value: typing.Any, /) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. self._metadata[key] = value return self def set_slash_hooks(self: _ComponentT, hooks_: typing.Optional[tanjun_abc.SlashHooks], /) -> _ComponentT: self._slash_hooks = hooks_ return self def set_message_hooks(self: _ComponentT, hooks_: typing.Optional[tanjun_abc.MessageHooks], /) -> _ComponentT: self._message_hooks = hooks_ return self def set_hooks(self: _ComponentT, hooks: typing.Optional[tanjun_abc.AnyHooks], /) -> _ComponentT: self._hooks = hooks return self def add_check(self: _ComponentT, check: tanjun_abc.CheckSig, /) -> _ComponentT: if check not in self._checks: self._checks.append(checks_.InjectableCheck(check)) return self def remove_check(self: _ComponentT, check: tanjun_abc.CheckSig, /) -> _ComponentT: self._checks.remove(typing.cast("checks_.InjectableCheck", check)) return self def with_check(self, check: tanjun_abc.CheckSigT, /) -> tanjun_abc.CheckSigT: self.add_check(check) return check def add_client_callback( self: _ComponentT, event_name: typing.Union[str, tanjun_abc.ClientCallbackNames], callback: tanjun_abc.MetaEventSig, /, ) -> _ComponentT: event_name = event_name.lower() try: if callback in self._client_callbacks[event_name]: return self self._client_callbacks[event_name].append(callback) except KeyError: self._client_callbacks[event_name] = [callback] if self._client: self._client.add_client_callback(event_name, callback) return self def get_client_callbacks( self, event_name: typing.Union[str, tanjun_abc.ClientCallbackNames], / ) -> collections.Collection[tanjun_abc.MetaEventSig]: event_name = event_name.lower() return self._client_callbacks.get(event_name) or () def remove_client_callback(self, event_name: str, callback: tanjun_abc.MetaEventSig, /) -> None: event_name = event_name.lower() self._client_callbacks[event_name].remove(callback) if not self._client_callbacks[event_name]: del self._client_callbacks[event_name] if self._client: self._client.remove_client_callback(event_name, callback) def with_client_callback( self, event_name: typing.Union[str, tanjun_abc.ClientCallbackNames], / ) -> collections.Callable[[tanjun_abc.MetaEventSigT], tanjun_abc.MetaEventSigT]: def decorator(callback: tanjun_abc.MetaEventSigT, /) -> tanjun_abc.MetaEventSigT: self.add_client_callback(event_name, callback) return callback return decorator def add_command(self: _ComponentT, command: tanjun_abc.ExecutableCommand[typing.Any], /) -> _ComponentT: """Add a command to this component. Parameters ---------- command : tanjun.abc.ExecutableCommand[typing.Any] The command to add. Returns ------- Self The current component to allow for chaining. """ if isinstance(command, tanjun_abc.MessageCommand): self.add_message_command(command) elif isinstance(command, tanjun_abc.BaseSlashCommand): self.add_slash_command(command) else: raise ValueError( f"Unexpected object passed, expected a MessageCommand or BaseSlashCommand but got {type(command)}" ) return self def remove_command(self: _ComponentT, command: tanjun_abc.ExecutableCommand[typing.Any], /) -> _ComponentT: """Remove a command from this component. Parameters ---------- command : tanjun.abc.ExecutableCommand[typing.Any] The command to remove. Returns ------- Self This component to enable method chaining. """ if isinstance(command, tanjun_abc.MessageCommand): self.remove_message_command(command) elif isinstance(command, tanjun_abc.BaseSlashCommand): self.remove_slash_command(command) else: raise ValueError( f"Unexpected object passed, expected a MessageCommand or BaseSlashCommand but got {type(command)}" ) return self @typing.overload def with_command(self, command: CommandT, /) -> CommandT: ... @typing.overload def with_command(self, /, *, copy: bool = False) -> collections.Callable[[CommandT], CommandT]: ... def with_command( self, command: typing.Optional[CommandT] = None, /, *, copy: bool = False ) -> WithCommandReturnSig[CommandT]: """Add a command to this component through a decorator call. Examples -------- This may be used inconjunction with `tanjun.as_slash_command` and `tanjun.as_message_command`. ```py @component.with_command @tanjun.with_slash_str_option("option_name", "option description") @tanjun.as_slash_command("command_name", "command description") async def slash_command(ctx: tanjun.abc.Context, arg: str) -> None: await ctx.respond(f"Hi {arg}") ``` ```py @component.with_command @tanjun.with_argument("argument_name") @tanjun.as_message_command("command_name") async def message_command(ctx: tanjun.abc.Context, arg: str) -> None: await ctx.respond(f"Hi {arg}") ``` Parameters ---------- command CommandT The command to add to this component. Other Parameters ---------------- copy : bool Whether to copy the command before adding it to this component. Returns ------- CommandT The added command. """ return _with_command(self.add_command, command, copy=copy) def add_slash_command(self: _ComponentT, command: tanjun_abc.BaseSlashCommand, /) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. if self._slash_commands.get(command.name) == command: return self command.bind_component(self) if self._client: command.bind_client(self._client) self._slash_commands[command.name.casefold()] = command return self def remove_slash_command(self: _ComponentT, command: tanjun_abc.BaseSlashCommand, /) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. try: del self._slash_commands[command.name.casefold()] except KeyError: raise ValueError(f"Command {command.name} not found") from None return self @typing.overload def with_slash_command(self, command: tanjun_abc.BaseSlashCommandT, /) -> tanjun_abc.BaseSlashCommandT: ... @typing.overload def with_slash_command( self, /, *, copy: bool = False ) -> collections.Callable[[tanjun_abc.BaseSlashCommandT], tanjun_abc.BaseSlashCommandT]: ... def with_slash_command( self, command: typing.Optional[tanjun_abc.BaseSlashCommandT] = None, /, *, copy: bool = False ) -> WithCommandReturnSig[tanjun_abc.BaseSlashCommandT]: # <<inherited docstring from tanjun.abc.Component>>. return _with_command(self.add_slash_command, command, copy=copy) def add_message_command(self: _ComponentT, command: tanjun_abc.MessageCommand[typing.Any], /) -> _ComponentT: """Add a message command to the component. Parameters ---------- command : tanjun.abc.MessageCommand The command to add. Returns ------- Self The component to allow method chaining. Raises ------ ValueError If one of the command's name is already registered in a strict component. """ if command in self._message_commands: return self if self._is_strict: if any(" " in name for name in command.names): raise ValueError("Command name cannot contain spaces for this component implementation") if name_conflicts := self._names_to_commands.keys() & command.names: raise ValueError( "Sub-command names must be unique in a strict component. " "The following conflicts were found " + ", ".join(name_conflicts) ) self._names_to_commands.update((name, command) for name in command.names) self._message_commands.append(command) if self._client: command.bind_client(self._client) command.bind_component(self) return self def remove_message_command(self: _ComponentT, command: tanjun_abc.MessageCommand[typing.Any], /) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. self._message_commands.remove(command) if self._is_strict: for name in command.names: if self._names_to_commands.get(name) == command: del self._names_to_commands[name] return self @typing.overload def with_message_command(self, command: tanjun_abc.MessageCommandT, /) -> tanjun_abc.MessageCommandT: ... @typing.overload def with_message_command( self, /, *, copy: bool = False ) -> collections.Callable[[tanjun_abc.MessageCommandT], tanjun_abc.MessageCommandT]: ... def with_message_command( self, command: typing.Optional[tanjun_abc.MessageCommandT] = None, /, *, copy: bool = False ) -> WithCommandReturnSig[tanjun_abc.MessageCommandT]: # <<inherited docstring from tanjun.abc.Component>>. return _with_command(self.add_message_command, command, copy=copy) def add_listener( self: _ComponentT, event: type[base_events.Event], listener: tanjun_abc.ListenerCallbackSig, / ) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. try: if listener in self._listeners[event]: return self self._listeners[event].append(listener) except KeyError: self._listeners[event] = [listener] if self._client: self._client.add_listener(event, listener) return self def remove_listener( self: _ComponentT, event: type[base_events.Event], listener: tanjun_abc.ListenerCallbackSig, / ) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. self._listeners[event].remove(listener) if not self._listeners[event]: del self._listeners[event] if self._client: self._client.remove_listener(event, listener) return self # TODO: make event optional? def with_listener( self, event_type: type[base_events.Event] ) -> collections.Callable[[tanjun_abc.ListenerCallbackSigT], tanjun_abc.ListenerCallbackSigT]: # <<inherited docstring from tanjun.abc.Component>>. def decorator(callback: tanjun_abc.ListenerCallbackSigT) -> tanjun_abc.ListenerCallbackSigT: self.add_listener(event_type, callback) return callback return decorator def add_on_close(self: _ComponentT, callback: OnCallbackSig, /) -> _ComponentT: """Add a close callback to this component. .. note:: Unlike the closing and closed client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client. Parameters ---------- callback : OnCallbackSig The close callback to add to this component. This should take no positional arguments, return `None` and may take use injected dependencies. Returns ------- Self The component object to enable call chaining. """ self._on_close.append(injecting.CallbackDescriptor(callback)) return self def with_on_close(self, callback: OnCallbackSigT, /) -> OnCallbackSigT: """Add a close callback to this component through a decorator call. .. note:: Unlike the closing and closed client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client. Parameters ---------- callback : OnCallbackSig The close callback to add to this component. This should take no positional arguments, return `None` and may take use injected dependencies. Returns ------- OnCallbackSig The added close callback. """ self.add_on_close(callback) return callback def add_on_open(self: _ComponentT, callback: OnCallbackSig, /) -> _ComponentT: """Add a open callback to this component. .. note:: Unlike the starting and started client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client. Parameters ---------- callback : OnCallbackSig The open callback to add to this component. This should take no positional arguments, return `None` and may take use injected dependencies. Returns ------- Self The component object to enable call chaining. """ self._on_open.append(injecting.CallbackDescriptor(callback)) return self def with_on_open(self, callback: OnCallbackSigT, /) -> OnCallbackSigT: """Add a open callback to this component through a decorator call. .. note:: Unlike the starting and started client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client. Parameters ---------- callback : OnCallbackSig The open callback to add to this component. This should take no positional arguments, return `None` and may take use injected dependencies. Returns ------- OnCallbackSig The added open callback. """ self.add_on_open(callback) return callback def bind_client(self: _ComponentT, client: tanjun_abc.Client, /) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. if self._client: raise RuntimeError("Client already set") self._client = client for message_command in self._message_commands: message_command.bind_client(client) for slash_command in self._slash_commands.values(): slash_command.bind_client(client) for event, listeners in self._listeners.items(): for listener in listeners: self._client.add_listener(event, listener) for event_name, callbacks in self._client_callbacks.items(): for callback in callbacks: self._client.add_client_callback(event_name, callback) return self def unbind_client(self: _ComponentT, client: tanjun_abc.Client, /) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. if not self._client or self._client != client: raise RuntimeError("Component isn't bound to this client") for event, listeners in self._listeners.items(): for listener in listeners: try: self._client.remove_listener(event, listener) except (LookupError, ValueError): pass for event_name, callbacks in self._client_callbacks.items(): for callback in callbacks: try: self._client.remove_client_callback(event_name, callback) except (LookupError, ValueError): pass self._client = None return self async def _check_context(self, ctx: tanjun_abc.Context, /) -> bool: return await utilities.gather_checks(ctx, self._checks) async def _check_message_context( self, ctx: tanjun_abc.MessageContext, / ) -> collections.AsyncIterator[tuple[str, tanjun_abc.MessageCommand[typing.Any]]]: ctx.set_component(self) if self._is_strict: name = ctx.content.split(" ", 1)[0] command = self._names_to_commands.get(name) if command and await self._check_context(ctx) and await command.check_context(ctx): yield name, command else: ctx.set_component(None) return checks_run = False for name, command in self.check_message_name(ctx.content): if not checks_run: if not await self._check_context(ctx): return checks_run = True if await command.check_context(ctx): yield name, command ctx.set_component(None) def check_message_name( self, content: str, / ) -> collections.Iterator[tuple[str, tanjun_abc.MessageCommand[typing.Any]]]: # <<inherited docstring from tanjun.abc.Component>>. if self._is_strict: name = content.split(" ", 1)[0] if command := self._names_to_commands.get(name): yield name, command return for command in self._message_commands: if (name_ := utilities.match_prefix_names(content, command.names)) is not None: yield name_, command def check_slash_name(self, name: str, /) -> collections.Iterator[tanjun_abc.BaseSlashCommand]: # <<inherited docstring from tanjun.abc.Component>>. if command := self._slash_commands.get(name): yield command async def _execute_interaction( self, ctx: tanjun_abc.SlashContext, command: typing.Optional[tanjun_abc.BaseSlashCommand], /, *, hooks: typing.Optional[collections.MutableSet[tanjun_abc.SlashHooks]] = None, ) -> typing.Optional[collections.Awaitable[None]]: try: if not command or not await self._check_context(ctx) or not await command.check_context(ctx): return None except errors.HaltExecution: return asyncio.get_running_loop().create_task(ctx.mark_not_found()) except errors.CommandError as exc: await ctx.respond(exc.message) asyncio.get_running_loop().create_future().set_result(None) return None if self._slash_hooks: if hooks is None: hooks = set() hooks.add(self._slash_hooks) if self._hooks: if hooks is None: hooks = set() hooks.add(self._hooks) return asyncio.get_running_loop().create_task(command.execute(ctx, hooks=hooks)) # To ensure that ctx.set_ephemeral_default is called as soon as possible if # a match is found the public function is kept sync to avoid yielding # to the event loop until after this is set. def execute_interaction( self, ctx: tanjun_abc.SlashContext, /, *, hooks: typing.Optional[collections.MutableSet[tanjun_abc.SlashHooks]] = None, ) -> collections.Coroutine[typing.Any, typing.Any, typing.Optional[collections.Awaitable[None]]]: # <<inherited docstring from tanjun.abc.Component>>. command = self._slash_commands.get(ctx.interaction.command_name) if command: if command.defaults_to_ephemeral is not None: ctx.set_ephemeral_default(command.defaults_to_ephemeral) elif self._defaults_to_ephemeral is not None: ctx.set_ephemeral_default(self._defaults_to_ephemeral) return self._execute_interaction(ctx, command, hooks=hooks) async def execute_message( self, ctx: tanjun_abc.MessageContext, /, *, hooks: typing.Optional[collections.MutableSet[tanjun_abc.MessageHooks]] = None, ) -> bool: # <<inherited docstring from tanjun.abc.Component>>. async for name, command in self._check_message_context(ctx): ctx.set_triggering_name(name) ctx.set_content(ctx.content[len(name) :].lstrip()) ctx.set_component(self) # Only add our hooks if we're sure we'll be executing the command here. if self._message_hooks: if hooks is None: hooks = set() hooks.add(self._message_hooks) if self._hooks: if hooks is None: hooks = set() hooks.add(self._hooks) await command.execute(ctx, hooks=hooks) return True ctx.set_component(None) return False def _load_from_properties(self) -> None: for _, member in inspect.getmembers(self): if isinstance(member, AbstractComponentLoader): member.load_into_component(self) def add_schedule(self: _ComponentT, schedule: schedules.AbstractSchedule, /) -> _ComponentT: """Add a schedule to the component. Parameters ---------- schedule : tanjun.schedules.AbstractSchedule The schedule to add. Returns ------- Self The component itself for chaining. """ if self._client and self._loop: # TODO: upgrade this to the standard interface assert isinstance(self._client, injecting.InjectorClient) schedule.start(self._client, loop=self._loop) self._schedules.append(schedule) return self def remove_schedule(self: _ComponentT, schedule: schedules.AbstractSchedule, /) -> _ComponentT: """Remove a schedule from the component. Parameters ---------- schedule : tanjun.schedules.AbstractSchedule The schedule to remove Returns ------- Self The component itself for chaining. Raises ------ ValueError If the schedule isn't registered. """ if schedule.is_alive: schedule.stop() self._schedules.remove(schedule) return self def with_schedule(self, schedule: _ScheduleT, /) -> _ScheduleT: """Add a schedule to the component through a decorator call. Example ------- This may be used in conjunction with `tanjun.as_interval`. ```py @component.with_schedule @tanjun.as_interval(60) async def my_schedule(): print("I'm running every minute!") ``` Parameters ---------- schedule : schedules.AbstractSchedule The schedule to add. Returns ------- schedules.AbstractSchedule The added schedule. """ self.add_schedule(schedule) return schedule async def close(self, *, unbind: bool = False) -> None: # <<inherited docstring from tanjun.abc.Component>>. if not self._loop: raise RuntimeError("Component isn't active") assert self._client for schedule in self._schedules: if schedule.is_alive: schedule.stop() self._loop = None # TODO: upgrade this to the standard interface assert isinstance(self._client, injecting.InjectorClient) await asyncio.gather( *(callback.resolve(injecting.BasicInjectionContext(self._client)) for callback in self._on_close) ) if unbind: self.unbind_client(self._client) async def open(self) -> None: # <<inherited docstring from tanjun.abc.Component>>. if self._loop: raise RuntimeError("Component is already active") if not self._client: raise RuntimeError("Client isn't bound yet") self._loop = asyncio.get_running_loop() # TODO: upgrade this to the standard interface assert isinstance(self._client, injecting.InjectorClient) await asyncio.gather( *(callback.resolve(injecting.BasicInjectionContext(self._client)) for callback in self._on_open) ) for schedule in self._schedules: schedule.start(self._client, loop=self._loop) def make_loader(self, *, copy: bool = True) -> tanjun_abc.ClientLoader: """Make a loader/unloader for this component. This enables loading, unloading and reloading of this component into a client by targeting the module using `tanjun.Client.load_modules`, `tanjun.Client.unload_modules` and `tanjun.Client.reload_modules`. Other Parameters ---------------- copy: bool Whether to copy the component before loading it into a client. Defaults to `True`. Returns ------- tanjun.abc.ClientLoader The loader for this component. """ return _ComponentManager(self, copy)
Standard implementation of tanjun.abc.Component.
This is a collcetion of commands (both message and slash), hooks and listener callbacks which can be added to a generic client.
Note: This implementation supports dependency injection for its checks, command callbacks and listeners when linked to a client which supports dependency injection.
View Source
def __init__(self, *, name: typing.Optional[str] = None, strict: bool = False) -> None: """Initialise a new component. Other Parameters ---------------- name : str The component's identifier. If not provided then this will be a random string. strict : bool Whether this component should use a stricter (more optimal) approach for message command search. When this is `True`, message command names will not be allowed to contain spaces and will have to be unique to one command within the component. """ self._checks: list[checks_.InjectableCheck] = [] self._client: typing.Optional[tanjun_abc.Client] = None self._client_callbacks: dict[str, list[tanjun_abc.MetaEventSig]] = {} self._defaults_to_ephemeral: typing.Optional[bool] = None self._hooks: typing.Optional[tanjun_abc.AnyHooks] = None self._is_strict = strict self._listeners: dict[type[base_events.Event], list[tanjun_abc.ListenerCallbackSig]] = {} self._loop: typing.Optional[asyncio.AbstractEventLoop] = None self._message_commands: list[tanjun_abc.MessageCommand[typing.Any]] = [] self._message_hooks: typing.Optional[tanjun_abc.MessageHooks] = None self._metadata: dict[typing.Any, typing.Any] = {} self._name = name or base64.b64encode(random.randbytes(32)).decode() self._names_to_commands: dict[str, tanjun_abc.MessageCommand[typing.Any]] = {} self._on_close: list[injecting.CallbackDescriptor[None]] = [] self._on_open: list[injecting.CallbackDescriptor[None]] = [] self._schedules: list[schedules.AbstractSchedule] = [] self._slash_commands: dict[str, tanjun_abc.BaseSlashCommand] = {} self._slash_hooks: typing.Optional[tanjun_abc.SlashHooks] = None
Initialise a new component.
Other Parameters
name (str): The component's identifier.
If not provided then this will be a random string.
strict (bool): Whether this component should use a stricter (more optimal) approach for message command search.
When this is
True, message command names will not be allowed to contain spaces and will have to be unique to one command within the component.
Collection of the checks being run against every command execution in this component.
Tanjun client this component is bound to.
Whether slash contexts executed in this component should default to ephemeral responses.
This effects calls to SlashContext.create_followup,
SlashContext.create_initial_response, SlashContext.defer and
SlashContext.respond unless the flags field is provided for the
methods which support it.
Notes
- This may be overridden by
BaseSlashCommand.defaults_to_ephemeral. - This only effects slash command execution.
- If this is
Nonethen the default from the parent client is used.
The asyncio loop this client is bound to if it has been opened.
Component's unique identifier.
Note: This will be preserved between copies of a component.
Collection of the schedules registered to this component.
Collection of the slash commands in this component.
Collection of the message commands in this component.
Whether any of the checks in this component require dependency injection.
Mapping of event types to the listeners registered for them in this component.
Mutable mapping of the metadata set for this component.
Note: Any modifications made to this mutable mapping will be preserved by the component.
View Source
def copy(self: _ComponentT, *, _new: bool = True) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. if not _new: self._checks = [check.copy() for check in self._checks] self._slash_commands = {name: command.copy() for name, command in self._slash_commands.items()} self._hooks = self._hooks.copy() if self._hooks else None self._listeners = { event: [copy.copy(listener) for listener in listeners] for event, listeners in self._listeners.items() } commands = {command: command.copy() for command in self._message_commands} self._message_commands = list(commands.values()) self._metadata = self._metadata.copy() self._names_to_commands = {name: commands[command] for name, command in self._names_to_commands.items()} self._schedules = [schedule.copy() for schedule in self._schedules] if self._schedules else [] return self return copy.copy(self).copy(_new=False)
View Source
def load_from_scope( self: _ComponentT, *, include_globals: bool = False, scope: typing.Optional[collections.Mapping[str, typing.Any]] = None, ) -> _ComponentT: """Load entries such as top-level commands into the component from the calling scope. Notes ----- * This will load schedules which support `AbstractComponentLoader` (e.g. `tanjun.schedules.IntervalSchedule`). * This will ignore commands which are owned by command groups. * This will detect entries from the calling scope which implement `AbstractComponentLoader` unless `scope` is passed but this isn't possible in a stack-less python implementation; in stack-less environments the scope will have to be explicitly passed as `scope`. Other Parameters ---------------- include_globals: bool Whether to include global variables (along with local) while detecting from the calling scope. This defaults to `False`, cannot be `True` when `scope` is provided and will only ever be needed when the local scope is different from the global scope. scope : typing.Optional[collections.Mapping[str, typing.Any]] The scope to detect entries which implement `AbstractComponentLoader` from. This overrides the default usage of stackframe introspection. Returns ------- Self The current component to allow for chaining. Raises ------ RuntimeError If this is called in a python implementation which doesn't support stack frame inspection when `scope` is not provided. ValueError If `scope` is provided when `include_globals` is True. """ if scope is None: if not (stack := inspect.currentframe()) or not stack.f_back: raise RuntimeError( "Stackframe introspection is not supported in this runtime. Please explicitly pass `scope`." ) values_iter = _filter_scope(stack.f_back.f_locals) if include_globals: values_iter = itertools.chain(values_iter, _filter_scope(stack.f_back.f_globals)) elif include_globals: raise ValueError("Cannot specify include_globals as True when scope is passed") else: values_iter = _filter_scope(scope) _LOGGER.info( "Loading commands for %s component from %s parent scope(s)", self.name, "global and local" if include_globals else "local", ) for value in values_iter: if isinstance(value, AbstractComponentLoader): value.load_into_component(self) return self
Load entries such as top-level commands into the component from the calling scope.
Notes
- This will load schedules which support
AbstractComponentLoader(e.g.tanjun.schedules.IntervalSchedule). - This will ignore commands which are owned by command groups.
- This will detect entries from the calling scope which implement
AbstractComponentLoaderunlessscopeis passed but this isn't possible in a stack-less python implementation; in stack-less environments the scope will have to be explicitly passed asscope.
Other Parameters
include_globals (bool): Whether to include global variables (along with local) while detecting from the calling scope.
This defaults to
False, cannot beTruewhenscopeis provided and will only ever be needed when the local scope is different from the global scope.scope (typing.Optional[collections.Mapping[str, typing.Any]]): The scope to detect entries which implement
AbstractComponentLoaderfrom.This overrides the default usage of stackframe introspection.
Returns
- Self: The current component to allow for chaining.
Raises
- RuntimeError: If this is called in a python implementation which doesn't support
stack frame inspection when
scopeis not provided. - ValueError: If
scopeis provided wheninclude_globalsis True.
View Source
def set_ephemeral_default(self: _ComponentT, state: typing.Optional[bool], /) -> _ComponentT: """Set whether slash contexts executed in this component should default to ephemeral responses. Parameters ---------- typing.Optional[bool] Whether slash command contexts executed in this component should should default to ephemeral. This will be overridden by any response calls which specify flags. Setting this to `None` will let the default set on the parent client propagate and decide the ephemeral default behaviour. Returns ------- SelfT This component to enable method chaining. """ self._defaults_to_ephemeral = state return self
Set whether slash contexts executed in this component should default to ephemeral responses.
Parameters
- typing.Optional[bool]: Whether slash command contexts executed in this component should should default to ephemeral. This will be overridden by any response calls which specify flags.
Setting this to None will let the default set on the parent
client propagate and decide the ephemeral default behaviour.
Returns
- SelfT: This component to enable method chaining.
View Source
def set_metadata(self: _ComponentT, key: typing.Any, value: typing.Any, /) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. self._metadata[key] = value return self
Set a field in the component's metadata.
Parameters
- key (typing.Any): Metadata key to set.
- value (typing.Any): Metadata value to set.
Returns
- Self: The component instance to enable chained calls.
View Source
def set_slash_hooks(self: _ComponentT, hooks_: typing.Optional[tanjun_abc.SlashHooks], /) -> _ComponentT: self._slash_hooks = hooks_ return self
View Source
def set_message_hooks(self: _ComponentT, hooks_: typing.Optional[tanjun_abc.MessageHooks], /) -> _ComponentT: self._message_hooks = hooks_ return self
View Source
def set_hooks(self: _ComponentT, hooks: typing.Optional[tanjun_abc.AnyHooks], /) -> _ComponentT: self._hooks = hooks return self
View Source
def add_check(self: _ComponentT, check: tanjun_abc.CheckSig, /) -> _ComponentT: if check not in self._checks: self._checks.append(checks_.InjectableCheck(check)) return self
View Source
def remove_check(self: _ComponentT, check: tanjun_abc.CheckSig, /) -> _ComponentT: self._checks.remove(typing.cast("checks_.InjectableCheck", check)) return self
View Source
def with_check(self, check: tanjun_abc.CheckSigT, /) -> tanjun_abc.CheckSigT: self.add_check(check) return check
View Source
def add_client_callback( self: _ComponentT, event_name: typing.Union[str, tanjun_abc.ClientCallbackNames], callback: tanjun_abc.MetaEventSig, /, ) -> _ComponentT: event_name = event_name.lower() try: if callback in self._client_callbacks[event_name]: return self self._client_callbacks[event_name].append(callback) except KeyError: self._client_callbacks[event_name] = [callback] if self._client: self._client.add_client_callback(event_name, callback) return self
View Source
def get_client_callbacks( self, event_name: typing.Union[str, tanjun_abc.ClientCallbackNames], / ) -> collections.Collection[tanjun_abc.MetaEventSig]: event_name = event_name.lower() return self._client_callbacks.get(event_name) or ()
View Source
def remove_client_callback(self, event_name: str, callback: tanjun_abc.MetaEventSig, /) -> None: event_name = event_name.lower() self._client_callbacks[event_name].remove(callback) if not self._client_callbacks[event_name]: del self._client_callbacks[event_name] if self._client: self._client.remove_client_callback(event_name, callback)
View Source
def with_client_callback( self, event_name: typing.Union[str, tanjun_abc.ClientCallbackNames], / ) -> collections.Callable[[tanjun_abc.MetaEventSigT], tanjun_abc.MetaEventSigT]: def decorator(callback: tanjun_abc.MetaEventSigT, /) -> tanjun_abc.MetaEventSigT: self.add_client_callback(event_name, callback) return callback return decorator
View Source
def add_command(self: _ComponentT, command: tanjun_abc.ExecutableCommand[typing.Any], /) -> _ComponentT: """Add a command to this component. Parameters ---------- command : tanjun.abc.ExecutableCommand[typing.Any] The command to add. Returns ------- Self The current component to allow for chaining. """ if isinstance(command, tanjun_abc.MessageCommand): self.add_message_command(command) elif isinstance(command, tanjun_abc.BaseSlashCommand): self.add_slash_command(command) else: raise ValueError( f"Unexpected object passed, expected a MessageCommand or BaseSlashCommand but got {type(command)}" ) return self
Add a command to this component.
Parameters
- command (tanjun.abc.ExecutableCommand[typing.Any]): The command to add.
Returns
- Self: The current component to allow for chaining.
View Source
def remove_command(self: _ComponentT, command: tanjun_abc.ExecutableCommand[typing.Any], /) -> _ComponentT: """Remove a command from this component. Parameters ---------- command : tanjun.abc.ExecutableCommand[typing.Any] The command to remove. Returns ------- Self This component to enable method chaining. """ if isinstance(command, tanjun_abc.MessageCommand): self.remove_message_command(command) elif isinstance(command, tanjun_abc.BaseSlashCommand): self.remove_slash_command(command) else: raise ValueError( f"Unexpected object passed, expected a MessageCommand or BaseSlashCommand but got {type(command)}" ) return self
Remove a command from this component.
Parameters
- command (tanjun.abc.ExecutableCommand[typing.Any]): The command to remove.
Returns
- Self: This component to enable method chaining.
View Source
def with_command( self, command: typing.Optional[CommandT] = None, /, *, copy: bool = False ) -> WithCommandReturnSig[CommandT]: """Add a command to this component through a decorator call. Examples -------- This may be used inconjunction with `tanjun.as_slash_command` and `tanjun.as_message_command`. ```py @component.with_command @tanjun.with_slash_str_option("option_name", "option description") @tanjun.as_slash_command("command_name", "command description") async def slash_command(ctx: tanjun.abc.Context, arg: str) -> None: await ctx.respond(f"Hi {arg}") ``` ```py @component.with_command @tanjun.with_argument("argument_name") @tanjun.as_message_command("command_name") async def message_command(ctx: tanjun.abc.Context, arg: str) -> None: await ctx.respond(f"Hi {arg}") ``` Parameters ---------- command CommandT The command to add to this component. Other Parameters ---------------- copy : bool Whether to copy the command before adding it to this component. Returns ------- CommandT The added command. """ return _with_command(self.add_command, command, copy=copy)
Add a command to this component through a decorator call.
Examples
This may be used inconjunction with tanjun.as_slash_command
and tanjun.as_message_command.
@component.with_command
@tanjun.with_slash_str_option("option_name", "option description")
@tanjun.as_slash_command("command_name", "command description")
async def slash_command(ctx: tanjun.abc.Context, arg: str) -> None:
await ctx.respond(f"Hi {arg}")
@component.with_command
@tanjun.with_argument("argument_name")
@tanjun.as_message_command("command_name")
async def message_command(ctx: tanjun.abc.Context, arg: str) -> None:
await ctx.respond(f"Hi {arg}")
Parameters
- command CommandT: The command to add to this component.
Other Parameters
- copy (bool): Whether to copy the command before adding it to this component.
Returns
- CommandT: The added command.
View Source
def add_slash_command(self: _ComponentT, command: tanjun_abc.BaseSlashCommand, /) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. if self._slash_commands.get(command.name) == command: return self command.bind_component(self) if self._client: command.bind_client(self._client) self._slash_commands[command.name.casefold()] = command return self
Add a slash command to this component.
Parameters
- command (BaseSlashCommand): The command to add.
Returns
- Self: The component to enable chained calls.
View Source
def remove_slash_command(self: _ComponentT, command: tanjun_abc.BaseSlashCommand, /) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. try: del self._slash_commands[command.name.casefold()] except KeyError: raise ValueError(f"Command {command.name} not found") from None return self
Remove a slash command from this component.
Parameters
- command (BaseSlashCommand): The command to remove.
Raises
- ValueError: If the provided command isn't found.
Returns
- Self: The component to enable chained calls.
View Source
def with_slash_command( self, command: typing.Optional[tanjun_abc.BaseSlashCommandT] = None, /, *, copy: bool = False ) -> WithCommandReturnSig[tanjun_abc.BaseSlashCommandT]: # <<inherited docstring from tanjun.abc.Component>>. return _with_command(self.add_slash_command, command, copy=copy)
Add a slash command to this component through a decorator call.
Parameters
- command (BaseSlashCommandT): The command to add.
Other Parameters
- copy (bool): Whether to copy the command before adding it.
Returns
- BaseSlashCommandT: The added command.
View Source
def add_message_command(self: _ComponentT, command: tanjun_abc.MessageCommand[typing.Any], /) -> _ComponentT: """Add a message command to the component. Parameters ---------- command : tanjun.abc.MessageCommand The command to add. Returns ------- Self The component to allow method chaining. Raises ------ ValueError If one of the command's name is already registered in a strict component. """ if command in self._message_commands: return self if self._is_strict: if any(" " in name for name in command.names): raise ValueError("Command name cannot contain spaces for this component implementation") if name_conflicts := self._names_to_commands.keys() & command.names: raise ValueError( "Sub-command names must be unique in a strict component. " "The following conflicts were found " + ", ".join(name_conflicts) ) self._names_to_commands.update((name, command) for name in command.names) self._message_commands.append(command) if self._client: command.bind_client(self._client) command.bind_component(self) return self
Add a message command to the component.
Parameters
- command (tanjun.abc.MessageCommand): The command to add.
Returns
- Self: The component to allow method chaining.
Raises
- ValueError: If one of the command's name is already registered in a strict component.
View Source
def remove_message_command(self: _ComponentT, command: tanjun_abc.MessageCommand[typing.Any], /) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. self._message_commands.remove(command) if self._is_strict: for name in command.names: if self._names_to_commands.get(name) == command: del self._names_to_commands[name] return self
Remove a message command from this component.
Parameters
- command (MessageCommand[typing.Any]): The command to remove.
Raises
- ValueError: If the provided command isn't found.
Returns
- Self: The component to enable chained calls.
View Source
def with_message_command( self, command: typing.Optional[tanjun_abc.MessageCommandT] = None, /, *, copy: bool = False ) -> WithCommandReturnSig[tanjun_abc.MessageCommandT]: # <<inherited docstring from tanjun.abc.Component>>. return _with_command(self.add_message_command, command, copy=copy)
Add a message command to this component through a decorator call.
Parameters
- command (MessageCommandT): The command to add.
Other Parameters
- copy (bool): Whether to copy the command before adding it.
Returns
- MessageCommandT: The added command.
View Source
def add_listener( self: _ComponentT, event: type[base_events.Event], listener: tanjun_abc.ListenerCallbackSig, / ) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. try: if listener in self._listeners[event]: return self self._listeners[event].append(listener) except KeyError: self._listeners[event] = [listener] if self._client: self._client.add_listener(event, listener) return self
Add a listener to this component.
Parameters
- event (type[hikari.Event]): The event to listen for.
- listener (ListenerCallbackSig): The listener to add.
Returns
- Self: The component to enable chained calls.
View Source
def remove_listener( self: _ComponentT, event: type[base_events.Event], listener: tanjun_abc.ListenerCallbackSig, / ) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. self._listeners[event].remove(listener) if not self._listeners[event]: del self._listeners[event] if self._client: self._client.remove_listener(event, listener) return self
Remove a listener from this component.
Parameters
- event (type[hikari.Event]): The event to listen for.
- listener (ListenerCallbackSig): The listener to remove.
Raises
- ValueError: If the listener is not registered for the provided event.
Returns
- Self: The component to enable chained calls.
View Source
def with_listener( self, event_type: type[base_events.Event] ) -> collections.Callable[[tanjun_abc.ListenerCallbackSigT], tanjun_abc.ListenerCallbackSigT]: # <<inherited docstring from tanjun.abc.Component>>. def decorator(callback: tanjun_abc.ListenerCallbackSigT) -> tanjun_abc.ListenerCallbackSigT: self.add_listener(event_type, callback) return callback return decorator
Add a listener to this component through a decorator call.
Parameters
- event_type (type[hikari.Event]): The event to listen for.
Returns
- collections.abc.Callable[[ListenerCallbackSigT], ListenerCallbackSigT]: Decorator callback which takes listener to add.
View Source
def add_on_close(self: _ComponentT, callback: OnCallbackSig, /) -> _ComponentT: """Add a close callback to this component. .. note:: Unlike the closing and closed client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client. Parameters ---------- callback : OnCallbackSig The close callback to add to this component. This should take no positional arguments, return `None` and may take use injected dependencies. Returns ------- Self The component object to enable call chaining. """ self._on_close.append(injecting.CallbackDescriptor(callback)) return self
Add a close callback to this component.
Note: Unlike the closing and closed client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client.
Parameters
callback (OnCallbackSig): The close callback to add to this component.
This should take no positional arguments, return
Noneand may take use injected dependencies.
Returns
- Self: The component object to enable call chaining.
View Source
def with_on_close(self, callback: OnCallbackSigT, /) -> OnCallbackSigT: """Add a close callback to this component through a decorator call. .. note:: Unlike the closing and closed client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client. Parameters ---------- callback : OnCallbackSig The close callback to add to this component. This should take no positional arguments, return `None` and may take use injected dependencies. Returns ------- OnCallbackSig The added close callback. """ self.add_on_close(callback) return callback
Add a close callback to this component through a decorator call.
Note: Unlike the closing and closed client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client.
Parameters
callback (OnCallbackSig): The close callback to add to this component.
This should take no positional arguments, return
Noneand may take use injected dependencies.
Returns
- OnCallbackSig: The added close callback.
View Source
def add_on_open(self: _ComponentT, callback: OnCallbackSig, /) -> _ComponentT: """Add a open callback to this component. .. note:: Unlike the starting and started client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client. Parameters ---------- callback : OnCallbackSig The open callback to add to this component. This should take no positional arguments, return `None` and may take use injected dependencies. Returns ------- Self The component object to enable call chaining. """ self._on_open.append(injecting.CallbackDescriptor(callback)) return self
Add a open callback to this component.
Note: Unlike the starting and started client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client.
Parameters
callback (OnCallbackSig): The open callback to add to this component.
This should take no positional arguments, return
Noneand may take use injected dependencies.
Returns
- Self: The component object to enable call chaining.
View Source
def with_on_open(self, callback: OnCallbackSigT, /) -> OnCallbackSigT: """Add a open callback to this component through a decorator call. .. note:: Unlike the starting and started client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client. Parameters ---------- callback : OnCallbackSig The open callback to add to this component. This should take no positional arguments, return `None` and may take use injected dependencies. Returns ------- OnCallbackSig The added open callback. """ self.add_on_open(callback) return callback
Add a open callback to this component through a decorator call.
Note: Unlike the starting and started client callbacks, this is only called for the current component's lifetime and is guaranteed to be called regardless of when the component was added to a client.
Parameters
callback (OnCallbackSig): The open callback to add to this component.
This should take no positional arguments, return
Noneand may take use injected dependencies.
Returns
- OnCallbackSig: The added open callback.
View Source
def bind_client(self: _ComponentT, client: tanjun_abc.Client, /) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. if self._client: raise RuntimeError("Client already set") self._client = client for message_command in self._message_commands: message_command.bind_client(client) for slash_command in self._slash_commands.values(): slash_command.bind_client(client) for event, listeners in self._listeners.items(): for listener in listeners: self._client.add_listener(event, listener) for event_name, callbacks in self._client_callbacks.items(): for callback in callbacks: self._client.add_client_callback(event_name, callback) return self
View Source
def unbind_client(self: _ComponentT, client: tanjun_abc.Client, /) -> _ComponentT: # <<inherited docstring from tanjun.abc.Component>>. if not self._client or self._client != client: raise RuntimeError("Component isn't bound to this client") for event, listeners in self._listeners.items(): for listener in listeners: try: self._client.remove_listener(event, listener) except (LookupError, ValueError): pass for event_name, callbacks in self._client_callbacks.items(): for callback in callbacks: try: self._client.remove_client_callback(event_name, callback) except (LookupError, ValueError): pass self._client = None return self
View Source
def check_message_name( self, content: str, / ) -> collections.Iterator[tuple[str, tanjun_abc.MessageCommand[typing.Any]]]: # <<inherited docstring from tanjun.abc.Component>>. if self._is_strict: name = content.split(" ", 1)[0] if command := self._names_to_commands.get(name): yield name, command return for command in self._message_commands: if (name_ := utilities.match_prefix_names(content, command.names)) is not None: yield name_, command
Check whether a name matches any of this component's registered message commands.
Notes
- This only checks for name matches against the top level command and will not account for sub-commands.
- Dependent on implementation detail this may partial check name against command names using name.startswith(command_name), hence why it also returns the name a command was matched by.
Parameters
- name (str): The name to check for command matches.
Returns
- collections.abc.Iterator[tuple[str, MessageCommand[typing.Any]]]: Iterator of tuples of command name matches to the relevant message command objects.
View Source
def check_slash_name(self, name: str, /) -> collections.Iterator[tanjun_abc.BaseSlashCommand]: # <<inherited docstring from tanjun.abc.Component>>. if command := self._slash_commands.get(name): yield command
Check whether a name matches any of this component's registered slash commands.
Note:
This won't check for sub-commands and will expect name to simply be
the top level command name.
Parameters
- name (str): The name to check for command matches.
Returns
- collections.abc.Iterator[BaseSlashCommand]: An iterator of the matching slash commands.
View Source
def execute_interaction( self, ctx: tanjun_abc.SlashContext, /, *, hooks: typing.Optional[collections.MutableSet[tanjun_abc.SlashHooks]] = None, ) -> collections.Coroutine[typing.Any, typing.Any, typing.Optional[collections.Awaitable[None]]]: # <<inherited docstring from tanjun.abc.Component>>. command = self._slash_commands.get(ctx.interaction.command_name) if command: if command.defaults_to_ephemeral is not None: ctx.set_ephemeral_default(command.defaults_to_ephemeral) elif self._defaults_to_ephemeral is not None: ctx.set_ephemeral_default(self._defaults_to_ephemeral) return self._execute_interaction(ctx, command, hooks=hooks)
Execute a slash context.
Note:
Unlike Component.execute_message, this shouldn't be expected to
raise tanjun.errors.HaltExecution nor tanjun.errors.CommandError.
Parameters
- ctx (SlashContext): The context to execute.
Other Parameters
- hooks (typing.Optional[collections.abc.MutableSet[SlashHooks]] = None): Set of hooks to include in this command execution.
Returns
- typing.Optional[collections.abc.Awaitable[None]]: Awaitable used to wait for the command execution to finish.
This may be awaited or left to run as a background task.
If this is None then the client should carry on its search for a
component with a matching command.
View Source
async def execute_message( self, ctx: tanjun_abc.MessageContext, /, *, hooks: typing.Optional[collections.MutableSet[tanjun_abc.MessageHooks]] = None, ) -> bool: # <<inherited docstring from tanjun.abc.Component>>. async for name, command in self._check_message_context(ctx): ctx.set_triggering_name(name) ctx.set_content(ctx.content[len(name) :].lstrip()) ctx.set_component(self) # Only add our hooks if we're sure we'll be executing the command here. if self._message_hooks: if hooks is None: hooks = set() hooks.add(self._message_hooks) if self._hooks: if hooks is None: hooks = set() hooks.add(self._hooks) await command.execute(ctx, hooks=hooks) return True ctx.set_component(None) return False
Execute a message context.
Parameters
- ctx (MessageContext): The context to execute.
Other Parameters
- hooks (typing.Optional[collections.abc.MutableSet[MessageHooks]] = None): Set of hooks to include in this command execution.
Returns
- bool: Whether a message command was executed in this component with the provided context.
If False then the client should carry on its search for a
component with a matching command.
Raises
- tanjun.errors.CommandError: To end the command's execution with an error response message.
- tanjun.errors.HaltExecution: To indicate that the client should stop searching for commands to execute with the current context.
View Source
def add_schedule(self: _ComponentT, schedule: schedules.AbstractSchedule, /) -> _ComponentT: """Add a schedule to the component. Parameters ---------- schedule : tanjun.schedules.AbstractSchedule The schedule to add. Returns ------- Self The component itself for chaining. """ if self._client and self._loop: # TODO: upgrade this to the standard interface assert isinstance(self._client, injecting.InjectorClient) schedule.start(self._client, loop=self._loop) self._schedules.append(schedule) return self
Add a schedule to the component.
Parameters
- schedule (tanjun.schedules.AbstractSchedule): The schedule to add.
Returns
- Self: The component itself for chaining.
View Source
def remove_schedule(self: _ComponentT, schedule: schedules.AbstractSchedule, /) -> _ComponentT: """Remove a schedule from the component. Parameters ---------- schedule : tanjun.schedules.AbstractSchedule The schedule to remove Returns ------- Self The component itself for chaining. Raises ------ ValueError If the schedule isn't registered. """ if schedule.is_alive: schedule.stop() self._schedules.remove(schedule) return self
Remove a schedule from the component.
Parameters
- schedule (tanjun.schedules.AbstractSchedule): The schedule to remove
Returns
- Self: The component itself for chaining.
Raises
- ValueError: If the schedule isn't registered.
View Source
def with_schedule(self, schedule: _ScheduleT, /) -> _ScheduleT: """Add a schedule to the component through a decorator call. Example ------- This may be used in conjunction with `tanjun.as_interval`. ```py @component.with_schedule @tanjun.as_interval(60) async def my_schedule(): print("I'm running every minute!") ``` Parameters ---------- schedule : schedules.AbstractSchedule The schedule to add. Returns ------- schedules.AbstractSchedule The added schedule. """ self.add_schedule(schedule) return schedule
Add a schedule to the component through a decorator call.
Example
This may be used in conjunction with tanjun.as_interval.
@component.with_schedule
@tanjun.as_interval(60)
async def my_schedule():
print("I'm running every minute!")
Parameters
- schedule (schedules.AbstractSchedule): The schedule to add.
Returns
- schedules.AbstractSchedule: The added schedule.
View Source
async def close(self, *, unbind: bool = False) -> None: # <<inherited docstring from tanjun.abc.Component>>. if not self._loop: raise RuntimeError("Component isn't active") assert self._client for schedule in self._schedules: if schedule.is_alive: schedule.stop() self._loop = None # TODO: upgrade this to the standard interface assert isinstance(self._client, injecting.InjectorClient) await asyncio.gather( *(callback.resolve(injecting.BasicInjectionContext(self._client)) for callback in self._on_close) ) if unbind: self.unbind_client(self._client)
Close the component.
Other Parameters
unbind (bool): Whether to unbind from the client after this is closed.
Defaults to
False.
Raises
- RuntimeError: If the component isn't running.
View Source
async def open(self) -> None: # <<inherited docstring from tanjun.abc.Component>>. if self._loop: raise RuntimeError("Component is already active") if not self._client: raise RuntimeError("Client isn't bound yet") self._loop = asyncio.get_running_loop() # TODO: upgrade this to the standard interface assert isinstance(self._client, injecting.InjectorClient) await asyncio.gather( *(callback.resolve(injecting.BasicInjectionContext(self._client)) for callback in self._on_open) ) for schedule in self._schedules: schedule.start(self._client, loop=self._loop)
Start the component.
Raises
- RuntimeError: If the component is already open. If the component isn't bound to a client.
View Source
def make_loader(self, *, copy: bool = True) -> tanjun_abc.ClientLoader: """Make a loader/unloader for this component. This enables loading, unloading and reloading of this component into a client by targeting the module using `tanjun.Client.load_modules`, `tanjun.Client.unload_modules` and `tanjun.Client.reload_modules`. Other Parameters ---------------- copy: bool Whether to copy the component before loading it into a client. Defaults to `True`. Returns ------- tanjun.abc.ClientLoader The loader for this component. """ return _ComponentManager(self, copy)
Make a loader/unloader for this component.
This enables loading, unloading and reloading of this component into a
client by targeting the module using tanjun.Client.load_modules,
tanjun.Client.unload_modules and tanjun.Client.reload_modules.
Other Parameters
copy (bool): Whether to copy the component before loading it into a client.
Defaults to
True.
Returns
- tanjun.abc.ClientLoader: The loader for this component.
View Source
# -*- coding: utf-8 -*- # cython: language_level=3 # BSD 3-Clause License # # Copyright (c) 2020-2022, Faster Speeding # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Standard command execution context implementations.""" from __future__ import annotations __all__: list[str] = ["MessageContext", "ResponseTypeT", "SlashContext", "SlashOption"] import asyncio import datetime import logging import typing import hikari from hikari import snowflakes from . import abc as tanjun_abc from . import injecting if typing.TYPE_CHECKING: from collections import abc as collections from hikari import traits as hikari_traits _BaseContextT = typing.TypeVar("_BaseContextT", bound="BaseContext") _MessageContextT = typing.TypeVar("_MessageContextT", bound="MessageContext") _SlashContextT = typing.TypeVar("_SlashContextT", bound="SlashContext") _T = typing.TypeVar("_T") ResponseTypeT = typing.Union[hikari.api.InteractionMessageBuilder, hikari.api.InteractionDeferredBuilder] """Union of the response types which are valid for application command interactions.""" _INTERACTION_LIFETIME: typing.Final[datetime.timedelta] = datetime.timedelta(minutes=15) _LOGGER = logging.getLogger("hikari.tanjun.context") def _delete_after_to_float(delete_after: typing.Union[datetime.timedelta, float, int]) -> float: return delete_after.total_seconds() if isinstance(delete_after, datetime.timedelta) else float(delete_after) class BaseContext(injecting.BasicInjectionContext, tanjun_abc.Context): """Base class for all standard context implementations.""" __slots__ = ("_client", "_component", "_final") def __init__( self, client: tanjun_abc.Client, injection_client: injecting.InjectorClient, *, component: typing.Optional[tanjun_abc.Component] = None, ) -> None: # injecting.BasicInjectionContext.__init__ super().__init__(injection_client) self._client = client self._component = component self._final = False ( self._set_type_special_case(tanjun_abc.Context, self) ._set_type_special_case(BaseContext, self) ._set_type_special_case(type(self), self) ) @property def cache(self) -> typing.Optional[hikari.api.Cache]: # <<inherited docstring from tanjun.abc.Context>>. return self._client.cache @property def client(self) -> tanjun_abc.Client: # <<inherited docstring from tanjun.abc.Context>>. return self._client @property def component(self) -> typing.Optional[tanjun_abc.Component]: # <<inherited docstring from tanjun.abc.Context>>. return self._component @property def events(self) -> typing.Optional[hikari.api.EventManager]: # <<inherited docstring from tanjun.abc.Context>>. return self._client.events @property def server(self) -> typing.Optional[hikari.api.InteractionServer]: # <<inherited docstring from tanjun.abc.Context>>. return self._client.server @property def rest(self) -> hikari.api.RESTClient: # <<inherited docstring from tanjun.abc.Context>>. return self._client.rest @property def shards(self) -> typing.Optional[hikari_traits.ShardAware]: # <<inherited docstring from tanjun.abc.Context>>. return self._client.shards @property def voice(self) -> typing.Optional[hikari.api.VoiceComponent]: # <<inherited docstring from tanjun.abc.Context>>. return self._client.voice def _assert_not_final(self) -> None: if self._final: raise TypeError("Cannot modify a finalised context") def finalise(self: _BaseContextT) -> _BaseContextT: """Finalise the context, dis-allowing any further modifications. Returns ------- Self The context itself to enable chained calls. """ self._final = True return self def set_component(self: _BaseContextT, component: typing.Optional[tanjun_abc.Component], /) -> _BaseContextT: # <<inherited docstring from tanjun.abc.Context>>. self._assert_not_final() if component: self._set_type_special_case(tanjun_abc.Component, component)._set_type_special_case( type(component), component ) elif component_case := self._special_case_types.get(tanjun_abc.Component): self._remove_type_special_case(tanjun_abc.Component) self._remove_type_special_case(type(component_case)) self._component = component return self def get_channel(self) -> typing.Optional[hikari.TextableGuildChannel]: # <<inherited docstring from tanjun.abc.Context>>. if self._client.cache: channel = self._client.cache.get_guild_channel(self.channel_id) assert channel is None or isinstance(channel, hikari.TextableGuildChannel) return channel return None def get_guild(self) -> typing.Optional[hikari.Guild]: # <<inherited docstring from tanjun.abc.Context>>. if self.guild_id is not None and self._client.cache: return self._client.cache.get_guild(self.guild_id) return None async def fetch_channel(self) -> hikari.TextableChannel: # <<inherited docstring from tanjun.abc.Context>>. channel = await self._client.rest.fetch_channel(self.channel_id) assert isinstance(channel, hikari.TextableChannel) return channel async def fetch_guild(self) -> typing.Optional[hikari.Guild]: # TODO: or raise? # <<inherited docstring from tanjun.abc.Context>>. if self.guild_id is not None: return await self._client.rest.fetch_guild(self.guild_id) return None class MessageContext(BaseContext, tanjun_abc.MessageContext): """Standard implementation of a command context as used within Tanjun.""" __slots__ = ( "_command", "_content", "_initial_response_id", "_last_response_id", "_response_lock", "_message", "_triggering_name", "_triggering_prefix", ) def __init__( self, client: tanjun_abc.Client, injection_client: injecting.InjectorClient, content: str, message: hikari.Message, *, command: typing.Optional[tanjun_abc.MessageCommand[typing.Any]] = None, component: typing.Optional[tanjun_abc.Component] = None, triggering_name: str = "", triggering_prefix: str = "", ) -> None: if message.content is None: raise ValueError("Cannot spawn context with a content-less message.") super().__init__(client, injection_client, component=component) self._command = command self._content = content self._initial_response_id: typing.Optional[hikari.Snowflake] = None self._last_response_id: typing.Optional[hikari.Snowflake] = None self._response_lock = asyncio.Lock() self._message = message self._triggering_name = triggering_name self._triggering_prefix = triggering_prefix self._set_type_special_case(tanjun_abc.MessageContext, self)._set_type_special_case(MessageContext, self) def __repr__(self) -> str: return f"MessageContext <{self._message!r}, {self._command!r}>" @property def author(self) -> hikari.User: # <<inherited docstring from tanjun.abc.Context>>. return self._message.author @property def channel_id(self) -> hikari.Snowflake: # <<inherited docstring from tanjun.abc.Context>>. return self._message.channel_id @property def command(self) -> typing.Optional[tanjun_abc.MessageCommand[typing.Any]]: # <<inherited docstring from tanjun.abc.MessageContext>>. return self._command @property def content(self) -> str: # <<inherited docstring from tanjun.abc.MessageContext>>. return self._content @property def created_at(self) -> datetime.datetime: # <<inherited docstring from tanjun.abc.Context>>. return self._message.created_at @property def guild_id(self) -> typing.Optional[hikari.Snowflake]: # <<inherited docstring from tanjun.abc.Context>>. return self._message.guild_id @property def has_responded(self) -> bool: # <<inherited docstring from tanjun.abc.Context>>. return self._initial_response_id is not None @property def is_human(self) -> bool: # <<inherited docstring from tanjun.abc.Context>>. return not self._message.author.is_bot and self._message.webhook_id is None @property def member(self) -> typing.Optional[hikari.Member]: # <<inherited docstring from tanjun.abc.Context>>. return self._message.member @property def message(self) -> hikari.Message: # <<inherited docstring from tanjun.abc.MessageContext>>. return self._message @property def triggering_name(self) -> str: # <<inherited docstring from tanjun.abc.Context>>. return self._triggering_name @property def triggering_prefix(self) -> str: # <<inherited docstring from tanjun.abc.MessageContext>>. return self._triggering_prefix @property def shard(self) -> typing.Optional[hikari.api.GatewayShard]: # <<inherited docstring from tanjun.abc.MessageContext>>. if not self._client.shards: return None if self._message.guild_id is not None: shard_id = snowflakes.calculate_shard_id(self._client.shards, self._message.guild_id) else: shard_id = 0 return self._client.shards.shards[shard_id] def set_command( self: _MessageContextT, command: typing.Optional[tanjun_abc.MessageCommand[typing.Any]], / ) -> _MessageContextT: # <<inherited docstring from tanjun.abc.MessageContext>>. self._assert_not_final() self._command = command if command: ( self._set_type_special_case(tanjun_abc.ExecutableCommand, command) ._set_type_special_case(tanjun_abc.MessageCommand, command) ._set_type_special_case(type(command), command) ) elif command_case := self._special_case_types.get(tanjun_abc.ExecutableCommand): self._remove_type_special_case(tanjun_abc.ExecutableCommand) self._remove_type_special_case(tanjun_abc.MessageCommand) # TODO: command group? self._remove_type_special_case(type(command_case)) return self def set_content(self: _MessageContextT, content: str, /) -> _MessageContextT: # <<inherited docstring from tanjun.abc.MessageContext>>. self._assert_not_final() self._content = content return self def set_triggering_name(self: _MessageContextT, name: str, /) -> _MessageContextT: # <<inherited docstring from tanjun.abc.MessageContext>>. self._assert_not_final() self._triggering_name = name return self def set_triggering_prefix(self: _MessageContextT, triggering_prefix: str, /) -> _MessageContextT: """Set the triggering prefix for this context. Parameters ---------- triggering_prefix : str The triggering prefix to set. Returns ------- Self This context to allow for chaining. """ self._assert_not_final() self._triggering_prefix = triggering_prefix return self async def delete_initial_response(self) -> None: # <<inherited docstring from tanjun.abc.Context>>. if self._initial_response_id is None: raise LookupError("Context has no initial response") await self._client.rest.delete_message(self._message.channel_id, self._initial_response_id) async def delete_last_response(self) -> None: # <<inherited docstring from tanjun.abc.Context>>. if self._last_response_id is None: raise LookupError("Context has no previous responses") await self._client.rest.delete_message(self._message.channel_id, self._last_response_id) async def edit_initial_response( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, delete_after: typing.Union[datetime.timedelta, float, int, None] = None, attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED, attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED, component: hikari.UndefinedNoneOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedNoneOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedNoneOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedNoneOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, replace_attachments: bool = False, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, ) -> hikari.Message: # <<inherited docstring from tanjun.abc.Context>>. delete_after = _delete_after_to_float(delete_after) if delete_after is not None else None if self._initial_response_id is None: raise LookupError("Context has no initial response") message = await self.rest.edit_message( self._message.channel_id, self._initial_response_id, content=content, attachment=attachment, attachments=attachments, component=component, components=components, embed=embed, embeds=embeds, replace_attachments=replace_attachments, mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, ) if delete_after is not None: asyncio.create_task(self._delete_after(delete_after, message)) return message async def edit_last_response( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, delete_after: typing.Union[datetime.timedelta, float, int, None] = None, attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED, attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED, component: hikari.UndefinedNoneOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedNoneOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedNoneOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedNoneOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, replace_attachments: bool = False, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, ) -> hikari.Message: # <<inherited docstring from tanjun.abc.Context>>. delete_after = _delete_after_to_float(delete_after) if delete_after is not None else None if self._last_response_id is None: raise LookupError("Context has no previous tracked response") message = await self.rest.edit_message( self._message.channel_id, self._last_response_id, content=content, attachment=attachment, attachments=attachments, component=component, components=components, embed=embed, embeds=embeds, replace_attachments=replace_attachments, mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, ) if delete_after is not None: asyncio.create_task(self._delete_after(delete_after, message)) return message async def fetch_initial_response(self) -> hikari.Message: # <<inherited docstring from tanjun.abc.Context>>. if self._initial_response_id is not None: return await self.client.rest.fetch_message(self._message.channel_id, self._initial_response_id) raise LookupError("No initial response found for this context") async def fetch_last_response(self) -> hikari.Message: # <<inherited docstring from tanjun.abc.Context>>. if self._last_response_id is not None: return await self.client.rest.fetch_message(self._message.channel_id, self._last_response_id) raise LookupError("No responses found for this context") @staticmethod async def _delete_after(delete_after: float, message: hikari.Message) -> None: await asyncio.sleep(delete_after) try: await message.delete() except hikari.NotFoundError as exc: _LOGGER.debug("Failed to delete response message after %.2f seconds", delete_after, exc_info=exc) async def respond( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, ensure_result: bool = True, delete_after: typing.Union[datetime.timedelta, float, int, None] = None, attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED, attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED, component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, tts: hikari.UndefinedOr[bool] = hikari.UNDEFINED, nonce: hikari.UndefinedOr[str] = hikari.UNDEFINED, reply: typing.Union[bool, hikari.SnowflakeishOr[hikari.PartialMessage], hikari.UndefinedType] = False, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, mentions_reply: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, ) -> hikari.Message: # <<inherited docstring from tanjun.abc.Context>>. delete_after = _delete_after_to_float(delete_after) if delete_after is not None else None async with self._response_lock: message = await self._message.respond( content=content, attachment=attachment, attachments=attachments, component=component, components=components, embed=embed, embeds=embeds, tts=tts, nonce=nonce, reply=reply, mentions_everyone=mentions_everyone, mentions_reply=mentions_reply, user_mentions=user_mentions, role_mentions=role_mentions, ) self._last_response_id = message.id if self._initial_response_id is None: self._initial_response_id = message.id if delete_after is not None: asyncio.create_task(self._delete_after(delete_after, message)) return message _SnowflakeOptions = { hikari.OptionType.USER, hikari.OptionType.MENTIONABLE, hikari.OptionType.ROLE, hikari.OptionType.CHANNEL, } class SlashOption(tanjun_abc.SlashOption): __slots__ = ("_interaction", "_option") def __init__(self, interaction: hikari.CommandInteraction, option: hikari.CommandInteractionOption, /): if option.value is None: raise ValueError("Cannot build a slash option with a value-less API representation") self._interaction = interaction self._option = option @property def name(self) -> str: # <<inherited docstring from tanjun.abc.SlashOption>>. return self._option.name @property def type(self) -> typing.Union[hikari.OptionType, int]: # <<inherited docstring from tanjun.abc.SlashOption>>. return self._option.type @property def value(self) -> typing.Union[str, int, hikari.Snowflake, bool, float]: # <<inherited docstring from tanjun.abc.SlashOption>>. # This is asserted in __init__ assert self._option.value is not None if self._option.type in _SnowflakeOptions: assert self._option.value is not None return hikari.Snowflake(self._option.value) return self._option.value def boolean(self) -> bool: # <<inherited docstring from tanjun.abc.SlashOption>>. if self.type is hikari.OptionType.BOOLEAN: return bool(self._option.value) raise TypeError("Option is not a boolean") def float(self) -> float: # <<inherited docstring from tanjun.abc.SlashOption>>. if self.type is hikari.OptionType.FLOAT: assert self._option.value is not None return float(self._option.value) raise TypeError("Option is not a float") def integer(self) -> int: # <<inherited docstring from tanjun.abc.SlashOption>>. if self.type is hikari.OptionType.INTEGER: assert self._option.value is not None return int(self._option.value) raise TypeError("Option is not an integer") def snowflake(self) -> hikari.Snowflake: # <<inherited docstring from tanjun.abc.SlashOption>>. if self.type in _SnowflakeOptions: assert self._option.value is not None return hikari.Snowflake(self._option.value) raise TypeError("Option is not a unique resource") def string(self) -> str: # <<inherited docstring from tanjun.abc.SlashOption>>. if self.type is hikari.OptionType.STRING: return str(self._option.value) raise TypeError("Option is not a string") def resolve_value( self, ) -> typing.Union[hikari.InteractionChannel, hikari.InteractionMember, hikari.Role, hikari.User]: # <<inherited docstring from tanjun.abc.SlashOption>>. if self._option.type is hikari.OptionType.CHANNEL: return self.resolve_to_channel() if self._option.type is hikari.OptionType.ROLE: return self.resolve_to_role() if self._option.type is hikari.OptionType.USER: return self.resolve_to_user() if self._option.type is hikari.OptionType.MENTIONABLE: return self.resolve_to_mentionable() raise TypeError(f"Option type {self._option.type} isn't resolvable") def resolve_to_channel(self) -> hikari.InteractionChannel: # <<inherited docstring from tanjun.abc.SlashOption>>. # What does self.value being None mean? if self._option.type is hikari.OptionType.CHANNEL: assert self._interaction.resolved assert self._option.value is not None return self._interaction.resolved.channels[hikari.Snowflake(self._option.value)] raise TypeError(f"Cannot resolve non-channel option type {self._option.type} to a user") @typing.overload def resolve_to_member(self) -> hikari.InteractionMember: ... @typing.overload def resolve_to_member(self, *, default: _T) -> typing.Union[hikari.InteractionMember, _T]: ... def resolve_to_member(self, *, default: _T = ...) -> typing.Union[hikari.InteractionMember, _T]: # <<inherited docstring from tanjun.abc.SlashOption>>. # What does self.value being None mean? if self._option.type is hikari.OptionType.USER: assert self._interaction.resolved assert self._option.value is not None if member := self._interaction.resolved.members.get(hikari.Snowflake(self._option.value)): return member if default is not ...: return default raise LookupError("User isn't in the current guild") from None if self._option.type is hikari.OptionType.MENTIONABLE: assert self._option.value is not None assert self._interaction.resolved target_id = hikari.Snowflake(self._option.value) if member := self._interaction.resolved.members.get(target_id): return member if target_id in self._interaction.resolved.users: if default is not ...: return default raise LookupError("User isn't in the current guild") raise TypeError(f"Cannot resolve non-user option type {self._option.type} to a member") def resolve_to_mentionable(self) -> typing.Union[hikari.Role, hikari.User, hikari.Member]: # <<inherited docstring from tanjun.abc.SlashOption>>. if self._option.type is hikari.OptionType.MENTIONABLE: assert self._option.value is not None assert self._interaction.resolved target_id = hikari.Snowflake(self._option.value) if role := self._interaction.resolved.roles.get(target_id): return role return self._interaction.resolved.members.get(target_id) or self._interaction.resolved.users[target_id] if self._option.type is hikari.OptionType.USER: return self.resolve_to_user() if self._option.type is hikari.OptionType.ROLE: return self.resolve_to_role() raise TypeError(f"Cannot resolve non-mentionable option type {self._option.type} to a mentionable entity.") def resolve_to_role(self) -> hikari.Role: # <<inherited docstring from tanjun.abc.SlashOption>>. if self._option.type is hikari.OptionType.ROLE: assert self._interaction.resolved assert self._option.value is not None return self._interaction.resolved.roles[hikari.Snowflake(self._option.value)] if self._option.type is hikari.OptionType.MENTIONABLE: assert self._interaction.resolved if role := self._interaction.resolved.roles.get(hikari.Snowflake(self.value)): return role raise TypeError(f"Cannot resolve non-role option type {self._option.type} to a role") def resolve_to_user(self) -> typing.Union[hikari.User, hikari.Member]: # <<inherited docstring from tanjun.abc.SlashOption>>. if self._option.type is hikari.OptionType.USER: assert self._interaction.resolved assert self._option.value is not None user_id = hikari.Snowflake(self._option.value) return self._interaction.resolved.members.get(user_id) or self._interaction.resolved.users[user_id] if self._option.type is hikari.OptionType.MENTIONABLE: assert self._interaction.resolved assert self._option.value is not None user_id = hikari.Snowflake(self._option.value) if result := self._interaction.resolved.members.get(user_id) or self._interaction.resolved.users.get( user_id ): return result raise TypeError(f"Cannot resolve non-user option type {self._option.type} to a user") _COMMAND_OPTION_TYPES: typing.Final[frozenset[hikari.OptionType]] = frozenset( [hikari.OptionType.SUB_COMMAND, hikari.OptionType.SUB_COMMAND_GROUP] ) class SlashContext(BaseContext, tanjun_abc.SlashContext): __slots__ = ( "_command", "_defaults_to_ephemeral", "_defer_task", "_has_been_deferred", "_has_responded", "_interaction", "_last_response_id", "_marked_not_found", "_on_not_found", "_options", "_response_future", "_response_lock", ) def __init__( self, client: tanjun_abc.Client, injection_client: injecting.InjectorClient, interaction: hikari.CommandInteraction, *, command: typing.Optional[tanjun_abc.BaseSlashCommand] = None, component: typing.Optional[tanjun_abc.Component] = None, default_to_ephemeral: bool = False, on_not_found: typing.Optional[collections.Callable[[SlashContext], collections.Awaitable[None]]] = None, ) -> None: super().__init__(client, injection_client, component=component) self._command = command self._defaults_to_ephemeral = default_to_ephemeral self._defer_task: typing.Optional[asyncio.Task[None]] = None self._has_been_deferred = False self._has_responded = False self._interaction = interaction self._last_response_id: typing.Optional[hikari.Snowflake] = None self._marked_not_found = False self._on_not_found = on_not_found self._response_future: typing.Optional[asyncio.Future[ResponseTypeT]] = None self._response_lock = asyncio.Lock() self._set_type_special_case(tanjun_abc.SlashContext, self)._set_type_special_case(SlashContext, self) options = interaction.options while options and (first_option := options[0]).type in _COMMAND_OPTION_TYPES: options = first_option.options if options: self._options = {option.name: SlashOption(interaction, option) for option in options} else: self._options = {} @property def author(self) -> hikari.User: # <<inherited docstring from tanjun.abc.Context>>. return self._interaction.user @property def channel_id(self) -> hikari.Snowflake: # <<inherited docstring from tanjun.abc.Context>>. return self._interaction.channel_id @property def client(self) -> tanjun_abc.Client: # <<inherited docstring from tanjun.abc.Context>>. return self._client @property def command(self) -> typing.Optional[tanjun_abc.BaseSlashCommand]: # <<inherited docstring from tanjun.abc.SlashContext>>. return self._command @property def created_at(self) -> datetime.datetime: # <<inherited docstring from tanjun.abc.Context>>. return self._interaction.created_at @property def defaults_to_ephemeral(self) -> bool: # <<inherited docstring from tanjun.abc.Context>>. return self._defaults_to_ephemeral @property def expires_at(self) -> datetime.datetime: # <<inherited docstring from tanjun.abc.SlashContext>>. return self.created_at + _INTERACTION_LIFETIME @property def guild_id(self) -> typing.Optional[hikari.Snowflake]: # <<inherited docstring from tanjun.abc.Context>>. return self._interaction.guild_id @property def has_been_deferred(self) -> bool: # <<inherited docstring from tanjun.abc.SlashContext>>. return self._has_been_deferred @property def has_responded(self) -> bool: # <<inherited docstring from tanjun.abc.Context>>. return self._has_responded @property def is_human(self) -> typing.Literal[True]: # <<inherited docstring from tanjun.abc.Context>>. return True @property def member(self) -> typing.Optional[hikari.InteractionMember]: # <<inherited docstring from tanjun.abc.Context>>. return self._interaction.member @property def triggering_name(self) -> str: # <<inherited docstring from tanjun.abc.Context>>. # TODO: account for command groups return self._interaction.command_name @property def interaction(self) -> hikari.CommandInteraction: # <<inherited docstring from tanjun.abc.SlashContext>>. return self._interaction @property def options(self) -> collections.Mapping[str, tanjun_abc.SlashOption]: # <<inherited docstring from tanjun.abc.SlashContext>>. return self._options.copy() async def _auto_defer(self, countdown: typing.Union[int, float], /) -> None: await asyncio.sleep(countdown) await self.defer() def cancel_defer(self) -> None: """Cancel the auto-deferral if its active.""" if self._defer_task: self._defer_task.cancel() def _get_flags( self, flags: typing.Union[hikari.UndefinedType, int, hikari.MessageFlag] = hikari.UNDEFINED ) -> typing.Union[int, hikari.MessageFlag]: if flags is hikari.UNDEFINED: return hikari.MessageFlag.EPHEMERAL if self._defaults_to_ephemeral else hikari.MessageFlag.NONE return flags or hikari.MessageFlag.NONE def get_response_future(self) -> asyncio.Future[ResponseTypeT]: """Get the future which will be used to set the initial response. .. note:: This will change the behaviour of this context to match the REST server flow. Returns ------- asyncio.Future[ResponseTypeT] The future which will be used to set the initial response. """ if not self._response_future: self._response_future = asyncio.get_running_loop().create_future() return self._response_future async def mark_not_found(self) -> None: # <<inherited docstring from tanjun.abc.SlashContext>>. # TODO: assert not finalised? if self._on_not_found and not self._marked_not_found: self._marked_not_found = True await self._on_not_found(self) def start_defer_timer(self: _SlashContextT, count_down: typing.Union[int, float], /) -> _SlashContextT: """Start the auto-deferral timer. Parameters ---------- count_down : typing.Union[int, float] The number of seconds to wait before automatically deferring the interaction. Returns ------- Self This context to allow for chaining. """ self._assert_not_final() if self._defer_task: raise RuntimeError("Defer timer already set") self._defer_task = asyncio.create_task(self._auto_defer(count_down)) return self def set_command(self: _SlashContextT, command: typing.Optional[tanjun_abc.BaseSlashCommand], /) -> _SlashContextT: # <<inherited docstring from tanjun.abc.SlashContext>>. self._assert_not_final() self._command = command if command: ( self._set_type_special_case(tanjun_abc.ExecutableCommand, command) ._set_type_special_case(tanjun_abc.BaseSlashCommand, command) ._set_type_special_case(tanjun_abc.SlashCommand, command) ._set_type_special_case(type(command), command) ) elif command_case := self._special_case_types.get(tanjun_abc.ExecutableCommand): self._remove_type_special_case(tanjun_abc.ExecutableCommand) self._remove_type_special_case(tanjun_abc.BaseSlashCommand) self._remove_type_special_case(tanjun_abc.SlashCommand) # TODO: command group? self._remove_type_special_case(type(command_case)) return self def set_ephemeral_default(self: _SlashContextT, state: bool, /) -> _SlashContextT: # <<inherited docstring from tanjun.abc.SlashContext>>. self._assert_not_final() # TODO: document not final assertions. self._defaults_to_ephemeral = state return self async def defer( self, flags: typing.Union[hikari.UndefinedType, int, hikari.MessageFlag] = hikari.UNDEFINED ) -> None: # <<inherited docstring from tanjun.abc.SlashContext>>. flags = self._get_flags(flags) in_defer_task = self._defer_task and self._defer_task is asyncio.current_task() if not in_defer_task: self.cancel_defer() async with self._response_lock: if self._has_been_deferred: if in_defer_task: return raise RuntimeError("Context has already been responded to") self._has_been_deferred = True if self._response_future: self._response_future.set_result(self._interaction.build_deferred_response().set_flags(flags)) else: await self._interaction.create_initial_response( hikari.ResponseType.DEFERRED_MESSAGE_CREATE, flags=flags ) def _validate_delete_after(self, delete_after: typing.Union[float, int, datetime.timedelta]) -> float: delete_after = _delete_after_to_float(delete_after) time_left = ( _INTERACTION_LIFETIME - (datetime.datetime.now(tz=datetime.timezone.utc) - self.created_at) ).total_seconds() if delete_after + 10 > time_left: raise ValueError("This interaction will have expired before delete_after is reached") return delete_after async def _delete_followup_after(self, delete_after: float, message: hikari.Message) -> None: await asyncio.sleep(delete_after) try: await self._interaction.delete_message(message) except hikari.NotFoundError as exc: _LOGGER.debug("Failed to delete response message after %.2f seconds", delete_after, exc_info=exc) async def _create_followup( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, delete_after: typing.Union[datetime.timedelta, float, int, None] = None, attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED, attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED, component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, tts: hikari.UndefinedOr[bool] = hikari.UNDEFINED, flags: typing.Union[hikari.UndefinedType, int, hikari.MessageFlag] = hikari.UNDEFINED, ) -> hikari.Message: delete_after = self._validate_delete_after(delete_after) if delete_after is not None else None message = await self._interaction.execute( content=content, attachment=attachment, attachments=attachments, component=component, components=components, embed=embed, embeds=embeds, flags=self._get_flags(flags), tts=tts, mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, ) self._last_response_id = message.id # This behaviour is undocumented and only kept by Discord for "backwards compatibility" # but the followup endpoint can be used to create the initial response for slash # commands or edit in a deferred response and (while this does lead to some # unexpected behaviour around deferrals) should be accounted for. if not self._has_responded: self._has_responded = True if delete_after is not None and not message.flags & hikari.MessageFlag.EPHEMERAL: asyncio.create_task(self._delete_followup_after(delete_after, message)) return message async def create_followup( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, delete_after: typing.Union[datetime.timedelta, float, int, None] = None, attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED, attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED, component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, tts: hikari.UndefinedOr[bool] = hikari.UNDEFINED, flags: typing.Union[hikari.UndefinedType, int, hikari.MessageFlag] = hikari.UNDEFINED, ) -> hikari.Message: # <<inherited docstring from tanjun.abc.SlashContext>>. async with self._response_lock: return await self._create_followup( content=content, delete_after=delete_after, attachment=attachment, attachments=attachments, component=component, components=components, embed=embed, embeds=embeds, mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, tts=tts, flags=flags, ) async def _delete_initial_response_after(self, delete_after: float) -> None: await asyncio.sleep(delete_after) try: await self.delete_initial_response() except hikari.NotFoundError as exc: _LOGGER.debug("Failed to delete response message after %.2f seconds", delete_after, exc_info=exc) async def _create_initial_response( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, delete_after: typing.Union[datetime.timedelta, float, int, None] = None, component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, flags: typing.Union[int, hikari.MessageFlag, hikari.UndefinedType] = hikari.UNDEFINED, tts: hikari.UndefinedOr[bool] = hikari.UNDEFINED, ) -> None: flags = self._get_flags(flags) delete_after = self._validate_delete_after(delete_after) if delete_after is not None else None if self._has_responded: raise RuntimeError("Initial response has already been created") if self._has_been_deferred: raise RuntimeError( "edit_initial_response must be used to set the initial response after a context has been deferred" ) self.cancel_defer() self._has_responded = True if not self._response_future: await self._interaction.create_initial_response( response_type=hikari.ResponseType.MESSAGE_CREATE, content=content, component=component, components=components, embed=embed, embeds=embeds, flags=flags, tts=tts, mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, ) else: if component and components: raise ValueError("Only one of component or components may be passed") if embed and embeds: raise ValueError("Only one of embed or embeds may be passed") if component: assert not isinstance(component, hikari.UndefinedType) components = (component,) if embed: assert not isinstance(embed, hikari.UndefinedType) embeds = (embed,) content = str(content) if content is not hikari.UNDEFINED else hikari.UNDEFINED # Pyright doesn't properly support attrs and doesn't account for _ being removed from field # pre-fix in init. result = hikari.impl.InteractionMessageBuilder( type=hikari.ResponseType.MESSAGE_CREATE, # type: ignore content=content, # type: ignore components=components, # type: ignore embeds=embeds, # type: ignore flags=flags, # type: ignore is_tts=tts, # type: ignore mentions_everyone=mentions_everyone, # type: ignore user_mentions=user_mentions, # type: ignore role_mentions=role_mentions, # type: ignore ) # type: ignore if embeds is not hikari.UNDEFINED: for embed in embeds: result.add_embed(embed) self._response_future.set_result(result) if delete_after is not None and not flags & hikari.MessageFlag.EPHEMERAL: asyncio.create_task(self._delete_initial_response_after(delete_after)) async def create_initial_response( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, delete_after: typing.Union[datetime.timedelta, float, int, None] = None, component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, flags: typing.Union[int, hikari.MessageFlag, hikari.UndefinedType] = hikari.UNDEFINED, tts: hikari.UndefinedOr[bool] = hikari.UNDEFINED, ) -> None: # <<inherited docstring from tanjun.abc.Context>>. async with self._response_lock: await self._create_initial_response( delete_after=delete_after, content=content, component=component, components=components, embed=embed, embeds=embeds, mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, flags=flags, tts=tts, ) async def delete_initial_response(self) -> None: # <<inherited docstring from tanjun.abc.Context>>. await self._interaction.delete_initial_response() # If they defer then delete the initial response then this should be treated as having # an initial response to allow for followup responses. self._has_responded = True async def delete_last_response(self) -> None: # <<inherited docstring from tanjun.abc.Context>>. if self._last_response_id is None: if self._has_responded or self._has_been_deferred: await self._interaction.delete_initial_response() # If they defer then delete the initial response then this should be treated as having # an initial response to allow for followup responses. self._has_responded = True return raise LookupError("Context has no last response") await self._interaction.delete_message(self._last_response_id) async def edit_initial_response( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, delete_after: typing.Union[datetime.timedelta, float, int, None] = None, attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED, attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED, component: hikari.UndefinedNoneOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedNoneOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedNoneOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedNoneOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, replace_attachments: bool = False, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, ) -> hikari.Message: # <<inherited docstring from tanjun.abc.Context>>. delete_after = self._validate_delete_after(delete_after) if delete_after is not None else None message = await self._interaction.edit_initial_response( content=content, attachment=attachment, attachments=attachments, component=component, components=components, embed=embed, embeds=embeds, replace_attachments=replace_attachments, mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, ) self._has_responded = True if delete_after is not None and not message.flags & hikari.MessageFlag.EPHEMERAL: asyncio.create_task(self._delete_initial_response_after(delete_after)) return message async def edit_last_response( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, delete_after: typing.Union[datetime.timedelta, float, int, None] = None, attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED, attachments: hikari.UndefinedOr[collections.Sequence[hikari.Resourceish]] = hikari.UNDEFINED, component: hikari.UndefinedNoneOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedNoneOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedNoneOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedNoneOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, replace_attachments: bool = False, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, ) -> hikari.Message: # <<inherited docstring from tanjun.abc.Context>>. if self._last_response_id: delete_after = self._validate_delete_after(delete_after) if delete_after is not None else None message = await self._interaction.edit_message( self._last_response_id, content=content, attachment=attachment, attachments=attachments, component=component, components=components, embed=embed, embeds=embeds, replace_attachments=replace_attachments, mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, ) if delete_after is not None and not message.flags & hikari.MessageFlag.EPHEMERAL: asyncio.create_task(self._delete_followup_after(delete_after, message)) return message if self._has_responded or self._has_been_deferred: return await self.edit_initial_response( delete_after=delete_after, content=content, attachment=attachment, attachments=attachments, component=component, components=components, embed=embed, embeds=embeds, replace_attachments=replace_attachments, mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, ) raise LookupError("Context has no previous responses") async def fetch_initial_response(self) -> hikari.Message: # <<inherited docstring from tanjun.abc.Context>>. return await self._interaction.fetch_initial_response() async def fetch_last_response(self) -> hikari.Message: # <<inherited docstring from tanjun.abc.Context>>. if self._last_response_id is not None: return await self._interaction.fetch_message(self._last_response_id) if self._has_responded: return await self.fetch_initial_response() raise LookupError("Context has no previous known responses") @typing.overload async def respond( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, ensure_result: typing.Literal[False] = False, delete_after: typing.Union[datetime.timedelta, float, int, None] = None, component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, ) -> typing.Optional[hikari.Message]: ... @typing.overload async def respond( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, ensure_result: typing.Literal[True], delete_after: typing.Union[datetime.timedelta, float, int, None] = None, component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, ) -> hikari.Message: ... async def respond( self, content: hikari.UndefinedOr[typing.Any] = hikari.UNDEFINED, *, ensure_result: bool = False, delete_after: typing.Union[datetime.timedelta, float, int, None] = None, component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED, components: hikari.UndefinedOr[collections.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED, embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED, embeds: hikari.UndefinedOr[collections.Sequence[hikari.Embed]] = hikari.UNDEFINED, mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED, user_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialUser], bool] ] = hikari.UNDEFINED, role_mentions: hikari.UndefinedOr[ typing.Union[hikari.SnowflakeishSequence[hikari.PartialRole], bool] ] = hikari.UNDEFINED, ) -> typing.Optional[hikari.Message]: # <<inherited docstring from tanjun.abc.Context>>. async with self._response_lock: if self._has_responded: return await self._create_followup( content, delete_after=delete_after, component=component, components=components, embed=embed, embeds=embeds, mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, ) if self._has_been_deferred: return await self.edit_initial_response( delete_after=delete_after, content=content, component=component, components=components, embed=embed, embeds=embeds, mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, ) await self._create_initial_response( delete_after=delete_after, content=content, component=component, components=components, embed=embed, embeds=embeds, mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, ) if ensure_result: return await self._interaction.fetch_initial_response()
Standard command execution context implementations.
View Source
# -*- coding: utf-8 -*- # cython: language_level=3 # BSD 3-Clause License # # Copyright (c) 2020-2022, Faster Speeding # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Functions and classes used to enable more Discord oriented argument converters.""" from __future__ import annotations __all__: list[str] = [ "from_datetime", "parse_snowflake", "parse_channel_id", "parse_emoji_id", "parse_role_id", "parse_user_id", "search_snowflakes", "search_channel_ids", "search_emoji_ids", "search_role_ids", "search_user_ids", "to_bool", "to_channel", "to_color", "to_colour", "to_datetime", "to_emoji", "to_guild", "to_invite", "to_invite_with_metadata", "to_member", "to_presence", "to_role", "to_snowflake", "to_user", "to_voice_state", "ToChannel", "ToEmoji", "ToGuild", "ToInvite", "ToInviteWithMetadata", "ToMember", "ToPresence", "ToRole", "ToUser", "ToVoiceState", ] import abc import datetime import logging import operator import re import typing import urllib.parse as urlparse from collections import abc as collections import hikari from . import abc as tanjun_abc from . import injecting from .dependencies import async_cache if typing.TYPE_CHECKING: from . import parsing _ArgumentT = typing.Union[str, int, float] _ValueT = typing.TypeVar("_ValueT") _LOGGER = logging.getLogger("hikari.tanjun.conversion") class BaseConverter(typing.Generic[_ValueT], abc.ABC): """Base class for the standard converters. .. warning:: Inheriting from this is completely unnecessary and should be avoided for people using the library unless they know what they're doing. This is detail of the standard implementation and isn't guaranteed to work between implementations but will work for implementations which provide the standard dependency injection or special cased support for these. While it isn't necessary to subclass this to implement your own converters since dependency injection can be used to access fields like the current Context, this class introduces some niceties around stuff like state warnings. """ __slots__ = () __pdoc__: typing.ClassVar[dict[str, bool]] = { "async_cache": False, "cache_components": False, "intents": False, "requires_cache": False, "__pdoc__": False, } @property @abc.abstractmethod def async_caches(self) -> collections.Sequence[typing.Any]: """Collection of the asynchronous caches that this converter relies on. This will only be necessary if the suggested intents or cache_components aren't enabled for a converter which requires cache. """ @property @abc.abstractmethod def cache_components(self) -> hikari.CacheComponents: """Cache component(s) the converter takes advantage of. .. note:: Unless `BaseConverter.requires_cache` is `True`, these cache components aren't necessary but simply avoid the converter from falling back to REST requests. This will be `hikari.CacheComponents.NONE` if the converter doesn't make cache calls. """ @property @abc.abstractmethod def intents(self) -> hikari.Intents: """Gateway intents this converter takes advantage of. .. note:: This field is supplementary to `BaseConverter.cache_components` and is used to detect when the relevant component might not actually be being kept up to date or filled by gateway events. Unless `BaseConverter.requires_cache` is `True`, these intents being disabled won't stop this converter from working as it'll still fall back to REST requests. """ @property @abc.abstractmethod def requires_cache(self) -> bool: """Whether this converter relies on the relevant cache stores to work. If this is `True` then this converter will not function properly in an environment `BaseConverter.intents` or `BaseConverter.cache_components` isn't satisfied and will never fallback to REST requests. """ def check_client(self, client: tanjun_abc.Client, parent_name: str, /) -> None: """Check that this converter will work with the given client. This never raises any errors but simply warns the user if the converter is not compatible with the given client. Parameters ---------- client : tanjun.abc.Client The client to check against. parent_name : str The name of the converter's parent, used for warning messages. """ # TODO: upgrade this stuff to the standard interface assert isinstance(client, injecting.InjectorClient) if not client.cache or any(client.get_type_dependency(cls) is injecting.UNDEFINED for cls in self.async_caches): if self.requires_cache: _LOGGER.warning( f"Converter {self!r} registered with {parent_name} will always fail with a stateless client.", ) elif self.cache_components: _LOGGER.warning( f"Converter {self!r} registered with {parent_name} may not perform optimally in a stateless client.", ) # elif missing_components := (self.cache_components & ~client.cache.components): # _LOGGER.warning( if client.shards and (missing_intents := self.intents & ~client.shards.intents): _LOGGER.warning( f"Converter {self!r} registered with {parent_name} may not perform as expected " f"without the following intents: {missing_intents}", ) _DmCacheT = typing.Optional[async_cache.SfCache[hikari.DMChannel]] _GuildChannelCacheT = typing.Optional[async_cache.SfCache[hikari.PartialChannel]] # TODO: GuildChannelConverter class ToChannel(BaseConverter[hikari.PartialChannel]): """Standard converter for channels mentions/IDs. For a standard instance of this see `to_channel`. """ __slots__ = ("_include_dms",) def __init__(self, *, include_dms: bool = True) -> None: """Initialise a to channel converter. Other Parameters ---------------- include_dms : bool Whether to include DM channels in the results. May lead to a lot of extra fallbacks to REST requests if the client doesn't have a registered async cache for DMs. Defaults to `True`. """ self._include_dms = include_dms @property def async_caches(self) -> collections.Sequence[typing.Any]: # <<inherited docstring from BaseConverter>>. return (_GuildChannelCacheT, _DmCacheT) @property def cache_components(self) -> hikari.CacheComponents: # <<inherited docstring from BaseConverter>>. return hikari.CacheComponents.GUILD_CHANNELS @property def intents(self) -> hikari.Intents: # <<inherited docstring from BaseConverter>>. return hikari.Intents.GUILDS @property def requires_cache(self) -> bool: # <<inherited docstring from BaseConverter>>. return False async def __call__( self, argument: _ArgumentT, /, ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context), cache: _GuildChannelCacheT = injecting.inject(type=_GuildChannelCacheT), dm_cache: _DmCacheT = injecting.inject(type=_DmCacheT), ) -> hikari.PartialChannel: channel_id = parse_channel_id(argument, message="No valid channel mention or ID found") if ctx.cache and (channel_ := ctx.cache.get_guild_channel(channel_id)): return channel_ no_guild_channel = False if cache: try: return await cache.get(channel_id) except async_cache.EntryNotFound: if not self._include_dms: raise ValueError("Couldn't find channel") from None no_guild_channel = True except async_cache.CacheMissError: pass if dm_cache and self._include_dms: try: return await dm_cache.get(channel_id) except async_cache.EntryNotFound: if no_guild_channel: raise ValueError("Couldn't find channel") from None except async_cache.CacheMissError: pass try: channel = await ctx.rest.fetch_channel(channel_id) if self._include_dms or isinstance(channel, hikari.GuildChannel): return channel except hikari.NotFoundError: pass raise ValueError("Couldn't find channel") ChannelConverter = ToChannel """Deprecated alias of `ToChannel`.""" _EmojiCacheT = typing.Optional[async_cache.SfCache[hikari.KnownCustomEmoji]] class ToEmoji(BaseConverter[hikari.KnownCustomEmoji]): """Standard converter for custom emojis. For a standard instance of this see `to_emoji`. .. note:: If you just want to convert inpute to a `hikari.Emoji`, `hikari.CustomEmoji` or `hikari.UnicodeEmoji` without making any cache or REST calls then you can just use the relevant `hikari.Emoji.parse`, `hikari.CustomEmoji.parse` or `hikari.UnicodeEmoji.parse` methods. """ __slots__ = () @property def async_caches(self) -> collections.Sequence[typing.Any]: # <<inherited docstring from BaseConverter>>. return (_EmojiCacheT,) @property def cache_components(self) -> hikari.CacheComponents: # <<inherited docstring from BaseConverter>>. return hikari.CacheComponents.EMOJIS @property def intents(self) -> hikari.Intents: # <<inherited docstring from BaseConverter>>. return hikari.Intents.GUILD_EMOJIS | hikari.Intents.GUILDS @property def requires_cache(self) -> bool: # <<inherited docstring from BaseConverter>>. return False async def __call__( self, argument: _ArgumentT, /, ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context), cache: _EmojiCacheT = injecting.inject(type=_EmojiCacheT), ) -> hikari.KnownCustomEmoji: emoji_id = parse_emoji_id(argument, message="No valid emoji or emoji ID found") if ctx.cache and (emoji := ctx.cache.get_emoji(emoji_id)): return emoji if cache: try: return await cache.get(emoji_id) except async_cache.EntryNotFound: raise ValueError("Couldn't find emoji") from None except async_cache.CacheMissError: pass if ctx.guild_id: try: return await ctx.rest.fetch_emoji(ctx.guild_id, emoji_id) except hikari.NotFoundError: pass raise ValueError("Couldn't find emoji") EmojiConverter = ToEmoji """Deprecated alias of `ToEmoji`.""" _GuildCacheT = typing.Optional[async_cache.SfCache[hikari.Guild]] class ToGuild(BaseConverter[hikari.Guild]): """Stanard converter for guilds. For a standard instance of this see `to_guild`. """ __slots__ = () @property def async_caches(self) -> collections.Sequence[typing.Any]: # <<inherited docstring from BaseConverter>>. return (_GuildCacheT,) @property def cache_components(self) -> hikari.CacheComponents: # <<inherited docstring from BaseConverter>>. return hikari.CacheComponents.GUILDS @property def intents(self) -> hikari.Intents: # <<inherited docstring from BaseConverter>>. return hikari.Intents.GUILDS @property def requires_cache(self) -> bool: # <<inherited docstring from BaseConverter>>. return False async def __call__( self, argument: _ArgumentT, /, ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context), cache: _GuildCacheT = injecting.inject(type=_GuildCacheT), ) -> hikari.Guild: guild_id = parse_snowflake(argument, message="No valid guild ID found") if ctx.cache and (guild := ctx.cache.get_guild(guild_id)): return guild if cache: try: return await cache.get(guild_id) except async_cache.EntryNotFound: raise ValueError("Couldn't find guild") from None except async_cache.CacheMissError: pass try: return await ctx.rest.fetch_guild(guild_id) except hikari.NotFoundError: pass raise ValueError("Couldn't find guild") GuildConverter = ToGuild """Deprecated alias of `ToGuild`.""" _InviteCacheT = typing.Optional[async_cache.AsyncCache[str, hikari.InviteWithMetadata]] class ToInvite(BaseConverter[hikari.Invite]): """Standard converter for invites.""" __slots__ = () @property def async_caches(self) -> collections.Sequence[typing.Any]: # <<inherited docstring from BaseConverter>>. return (_InviteCacheT,) @property def cache_components(self) -> hikari.CacheComponents: # <<inherited docstring from BaseConverter>>. return hikari.CacheComponents.INVITES @property def intents(self) -> hikari.Intents: # <<inherited docstring from BaseConverter>>. return hikari.Intents.GUILD_INVITES @property def requires_cache(self) -> bool: # <<inherited docstring from BaseConverter>>. return False async def __call__( self, argument: _ArgumentT, /, ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context), cache: _InviteCacheT = injecting.inject(type=_InviteCacheT), ) -> hikari.Invite: if not isinstance(argument, str): raise ValueError(f"`{argument}` is not a valid invite code") if ctx.cache and (invite := ctx.cache.get_invite(argument)): return invite if cache: try: return await cache.get(argument) except async_cache.EntryNotFound: raise ValueError("Couldn't find invite") from None except async_cache.CacheMissError: pass try: return await ctx.rest.fetch_invite(argument) except hikari.NotFoundError: pass raise ValueError("Couldn't find invite") InviteConverter = ToInvite """Deprecated alias of `ToInvite`.""" class ToInviteWithMetadata(BaseConverter[hikari.InviteWithMetadata]): """Standard converter for invites with metadata. For a standard instance of this see `to_invite_with_metadata`. .. note:: Unlike `InviteConverter`, this converter is cache dependent. """ __slots__ = () @property def async_caches(self) -> collections.Sequence[typing.Any]: # <<inherited docstring from BaseConverter>>. return (_InviteCacheT,) @property def cache_components(self) -> hikari.CacheComponents: # <<inherited docstring from BaseConverter>>. return hikari.CacheComponents.INVITES @property def intents(self) -> hikari.Intents: # <<inherited docstring from BaseConverter>>. return hikari.Intents.GUILD_INVITES @property def requires_cache(self) -> bool: # <<inherited docstring from BaseConverter>>. return True async def __call__( self, argument: _ArgumentT, /, ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context), cache: typing.Optional[_InviteCacheT] = injecting.inject(type=_InviteCacheT), ) -> hikari.InviteWithMetadata: if not isinstance(argument, str): raise ValueError(f"`{argument}` is not a valid invite code") if ctx.cache and (invite := ctx.cache.get_invite(argument)): return invite if cache and (invite := await cache.get(argument)): return invite raise ValueError("Couldn't find invite") InviteWithMetadataConverter = ToInviteWithMetadata """Deprecated alias of `ToInviteWithMetadata`.""" _MemberCacheT = typing.Optional[async_cache.SfGuildBound[hikari.Member]] class ToMember(BaseConverter[hikari.Member]): """Standard converter for guild members. For a standard instance of this see `to_member`. This converter allows both mentions, raw IDs and partial usernames/nicknames and only works within a guild context. """ __slots__ = () @property def async_caches(self) -> collections.Sequence[typing.Any]: # <<inherited docstring from BaseConverter>>. return (_MemberCacheT,) @property def cache_components(self) -> hikari.CacheComponents: # <<inherited docstring from BaseConverter>>. return hikari.CacheComponents.MEMBERS @property def intents(self) -> hikari.Intents: # <<inherited docstring from BaseConverter>>. return hikari.Intents.GUILD_MEMBERS | hikari.Intents.GUILDS @property def requires_cache(self) -> bool: # <<inherited docstring from BaseConverter>>. return False async def __call__( self, argument: _ArgumentT, /, ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context), cache: _MemberCacheT = injecting.inject(type=_MemberCacheT), ) -> hikari.Member: if ctx.guild_id is None: raise ValueError("Cannot get a member from a DM channel") try: user_id = parse_user_id(argument, message="No valid user mention or ID found") except ValueError: if isinstance(argument, str): try: return (await ctx.rest.search_members(ctx.guild_id, argument))[0] except (hikari.NotFoundError, IndexError): pass else: if ctx.cache and (member := ctx.cache.get_member(ctx.guild_id, user_id)): return member if cache: try: return await cache.get_from_guild(ctx.guild_id, user_id) except async_cache.EntryNotFound: raise ValueError("Couldn't find member in this guild") from None except async_cache.CacheMissError: pass try: return await ctx.rest.fetch_member(ctx.guild_id, user_id) except hikari.NotFoundError: pass raise ValueError("Couldn't find member in this guild") MemberConverter = ToMember """Deprecated alias of `ToMember`.""" _PresenceCacheT = typing.Optional[async_cache.SfGuildBound[hikari.MemberPresence]] class ToPresence(BaseConverter[hikari.MemberPresence]): """Standard converter for presences. For a standard instance of this see `to_presence`. This converter is cache dependent and only works in a guild context. """ __slots__ = () @property def async_caches(self) -> collections.Sequence[typing.Any]: # <<inherited docstring from BaseConverter>>. return (_PresenceCacheT,) @property def cache_components(self) -> hikari.CacheComponents: # <<inherited docstring from BaseConverter>>. return hikari.CacheComponents.PRESENCES @property def intents(self) -> hikari.Intents: # <<inherited docstring from BaseConverter>>. return hikari.Intents.GUILD_PRESENCES | hikari.Intents.GUILDS @property def requires_cache(self) -> bool: # <<inherited docstring from BaseConverter>>. return True async def __call__( self, argument: _ArgumentT, /, ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context), cache: _PresenceCacheT = injecting.inject(type=_PresenceCacheT), ) -> hikari.MemberPresence: if ctx.guild_id is None: raise ValueError("Cannot get a presence from a DM channel") user_id = parse_user_id(argument, message="No valid member mention or ID found") if ctx.cache and (presence := ctx.cache.get_presence(ctx.guild_id, user_id)): return presence if cache and (presence := await cache.get_from_guild(ctx.guild_id, user_id, default=None)): return presence raise ValueError("Couldn't find presence in current guild") PresenceConverter = ToPresence """Deprecated alias of `ToPresence`.""" _RoleCacheT = typing.Optional[async_cache.SfCache[hikari.Role]] class ToRole(BaseConverter[hikari.Role]): """Standard converter for guild roles. For a standard instance of this see `to_role`. """ __slots__ = () @property def async_caches(self) -> collections.Sequence[typing.Any]: # <<inherited docstring from BaseConverter>>. return (_RoleCacheT,) @property def cache_components(self) -> hikari.CacheComponents: # <<inherited docstring from BaseConverter>>. return hikari.CacheComponents.ROLES @property def intents(self) -> hikari.Intents: # <<inherited docstring from BaseConverter>>. return hikari.Intents.GUILDS @property def requires_cache(self) -> bool: # <<inherited docstring from BaseConverter>>. return False async def __call__( self, argument: _ArgumentT, /, ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context), cache: _RoleCacheT = injecting.inject(type=_RoleCacheT), ) -> hikari.Role: role_id = parse_role_id(argument, message="No valid role mention or ID found") if ctx.cache and (role := ctx.cache.get_role(role_id)): return role if cache: try: return await cache.get(role_id) except async_cache.EntryNotFound: raise ValueError("Couldn't find role") from None except async_cache.CacheMissError: pass if ctx.guild_id: for role in await ctx.rest.fetch_roles(ctx.guild_id): if role.id == role_id: return role raise ValueError("Couldn't find role") RoleConverter = ToRole """Deprecated alias of `ToRole`.""" _UserCacheT = typing.Optional[async_cache.SfCache[hikari.User]] class ToUser(BaseConverter[hikari.User]): """Standard converter for users. For a standard instance of this see `to_user`. """ __slots__ = () @property def async_caches(self) -> collections.Sequence[typing.Any]: # <<inherited docstring from BaseConverter>>. return (_UserCacheT,) @property def cache_components(self) -> hikari.CacheComponents: # <<inherited docstring from BaseConverter>>. return hikari.CacheComponents.NONE @property def intents(self) -> hikari.Intents: # <<inherited docstring from BaseConverter>>. return hikari.Intents.GUILDS | hikari.Intents.GUILD_MEMBERS @property def requires_cache(self) -> bool: # <<inherited docstring from BaseConverter>>. return False async def __call__( self, argument: _ArgumentT, /, ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context), cache: _UserCacheT = injecting.inject(type=_UserCacheT), ) -> hikari.User: # TODO: search by name if this is a guild context user_id = parse_user_id(argument, message="No valid user mention or ID found") if ctx.cache and (user := ctx.cache.get_user(user_id)): return user if cache: try: return await cache.get(user_id) except async_cache.EntryNotFound: raise ValueError("Couldn't find user") from None except async_cache.CacheMissError: pass try: return await ctx.rest.fetch_user(user_id) except hikari.NotFoundError: pass raise ValueError("Couldn't find user") UserConverter = ToUser """Deprecated alias of `ToUser`.""" _VoiceStateCacheT = typing.Optional[async_cache.SfGuildBound[hikari.VoiceState]] class ToVoiceState(BaseConverter[hikari.VoiceState]): """Standard converter for voice states. For a standard instance of this see `to_voice_state`. .. note:: This converter is cache dependent and only works in a guild context. """ __slots__ = () @property def async_caches(self) -> collections.Sequence[typing.Any]: # <<inherited docstring from BaseConverter>>. return (_VoiceStateCacheT,) @property def cache_components(self) -> hikari.CacheComponents: # <<inherited docstring from BaseConverter>>. return hikari.CacheComponents.VOICE_STATES @property def intents(self) -> hikari.Intents: # <<inherited docstring from BaseConverter>>. return hikari.Intents.GUILD_VOICE_STATES | hikari.Intents.GUILDS @property def requires_cache(self) -> bool: # <<inherited docstring from BaseConverter>>. return True async def __call__( self, argument: _ArgumentT, /, ctx: tanjun_abc.Context = injecting.inject(type=tanjun_abc.Context), cache: _VoiceStateCacheT = injecting.inject(type=_VoiceStateCacheT), ) -> hikari.VoiceState: if ctx.guild_id is None: raise ValueError("Cannot get a voice state from a DM channel") user_id = parse_user_id(argument, message="No valid user mention or ID found") if ctx.cache and (state := ctx.cache.get_voice_state(ctx.guild_id, user_id)): return state if cache and (state := await cache.get_from_guild(ctx.guild_id, user_id, default=None)): return state raise ValueError("Voice state couldn't be found for current guild") VoiceStateConverter = ToVoiceState """Deprecated alias of `ToVoiceState`.""" class _IDMatcherSig(typing.Protocol): def __call__(self, value: _ArgumentT, /, *, message: str = "No valid mention or ID found") -> hikari.Snowflake: raise NotImplementedError def _make_snowflake_parser(regex: re.Pattern[str], /) -> _IDMatcherSig: def parse(value: _ArgumentT, /, *, message: str = "No valid mention or ID found") -> hikari.Snowflake: """Parse a snowflake from a string or int value. .. note:: This only allows the relevant entity's mention format if applicable. Parameters ---------- value: typing.Union[str, int] The value to parse (this argument can only be passed positionally). Other Parameters ---------------- message: str The error message to raise if the value cannot be parsed. Returns ------- hikari.Snowflake The parsed snowflake. Raises ------ ValueError If the value cannot be parsed. """ result: typing.Optional[hikari.Snowflake] = None if isinstance(value, str): if value.isdigit(): result = hikari.Snowflake(value) else: capture = next(regex.finditer(value), None) result = hikari.Snowflake(capture.groups()[0]) if capture else None else: try: # Technically passing a float here is invalid (typing wise) # but we handle that by catching TypeError result = hikari.Snowflake(operator.index(typing.cast(int, value))) except (TypeError, ValueError): pass # We should also range check the provided ID. if result is not None and _range_check(result): return result raise ValueError(message) from None return parse _IDSearcherSig = collections.Callable[[_ArgumentT], collections.Iterator[hikari.Snowflake]] def _range_check(snowflake: hikari.Snowflake, /) -> bool: return snowflake.min() <= snowflake <= snowflake.max() def _make_snowflake_searcher(regex: re.Pattern[str], /) -> _IDSearcherSig: def parse(value: _ArgumentT, /) -> collections.Iterator[hikari.Snowflake]: """Iterate over the snowflakes in a string. .. note:: This only allows the relevant entity's mention format if applicable. Parameters ---------- value: typing.Union[str, int] The value to parse (this argument can only be passed positionally). Returns ------- collections.abc.Iterator[hikari.Snowflake] An iterator over the IDs found in the string. """ if isinstance(value, str): if value.isdigit() and _range_check(result := hikari.Snowflake(value)): yield result else: yield from filter( _range_check, map(hikari.Snowflake, (match.groups()[0] for match in regex.finditer(value))) ) yield from filter(_range_check, map(hikari.Snowflake, filter(str.isdigit, value.split()))) else: try: # Technically passing a float here is invalid (typing wise) # but we handle that by catching TypeError result = hikari.Snowflake(operator.index(typing.cast(int, value))) except (TypeError, ValueError): pass else: if _range_check(result): yield result return parse _SNOWFLAKE_REGEX = re.compile(r"<[@&?!#a]{0,3}(?::\w+:)?(\d+)>") parse_snowflake: _IDMatcherSig = _make_snowflake_parser(_SNOWFLAKE_REGEX) """Parse a snowflake from a string or int value. Parameters ---------- value: typing.Union[str, int] The value to parse (this argument can only be passed positionally). Other Parameters ---------------- message: str The error message to raise if the value cannot be parsed. Returns ------- hikari.Snowflake The parsed snowflake. Raises ------ ValueError If the value cannot be parsed. """ search_snowflakes: _IDSearcherSig = _make_snowflake_searcher(_SNOWFLAKE_REGEX) """Iterate over the snowflakes in a string. Parameters ---------- value: typing.Union[str, int] The value to parse (this argument can only be passed positionally). Returns ------- collections.abc.Iterator[hikari.Snowflake] An iterator over the snowflakes in the string. """ _CHANNEL_ID_REGEX = re.compile(r"<#(\d+)>") parse_channel_id: _IDMatcherSig = _make_snowflake_parser(_CHANNEL_ID_REGEX) """Parse a channel ID from a string or int value. Parameters ---------- value: typing.Union[str, int] The value to parse (this argument can only be passed positionally). Other Parameters ---------------- message: str The error message to raise if the value cannot be parsed. Returns ------- hikari.Snowflake The parsed channel ID. Raises ------ ValueError If the value cannot be parsed. """ search_channel_ids: _IDSearcherSig = _make_snowflake_searcher(_CHANNEL_ID_REGEX) """Iterate over the channel IDs in a string. Parameters ---------- value: typing.Union[str, int] The value to parse (this argument can only be passed positionally). Returns ------- collections.abc.Iterator[hikari.Snowflake] An iterator over the channel IDs in the string. """ _EMOJI_ID_REGEX = re.compile(r"<a?:\w+:(\d+)>") parse_emoji_id: _IDMatcherSig = _make_snowflake_parser(_EMOJI_ID_REGEX) """Parse an Emoji ID from a string or int value. Parameters ---------- value: typing.Union[str, int] The value to parse (this argument can only be passed positionally). Other Parameters ---------------- message: str The error message to raise if the value cannot be parsed. Returns ------- hikari.Snowflake The parsed Emoji ID. Raises ------ ValueError If the value cannot be parsed. """ search_emoji_ids: _IDSearcherSig = _make_snowflake_searcher(_EMOJI_ID_REGEX) """Iterate over the emoji IDs in a string. Parameters ---------- value: typing.Union[str, int] The value to parse (this argument can only be passed positionally). Returns ------- collections.abc.Iterator[hikari.Snowflake] An iterator over the emoji IDs in the string. """ _ROLE_ID_REGEX = re.compile(r"<@&(\d+)>") parse_role_id: _IDMatcherSig = _make_snowflake_parser(_ROLE_ID_REGEX) """Parse a role ID from a string or int value. Parameters ---------- value: typing.Union[str, int] The value to parse (this argument can only be passed positionally). Other Parameters ---------------- message: str The error message to raise if the value cannot be parsed. Returns ------- hikari.Snowflake The parsed role ID. Raises ------ ValueError If the value cannot be parsed. """ search_role_ids: _IDSearcherSig = _make_snowflake_searcher(_ROLE_ID_REGEX) """Iterate over the role IDs in a string. Parameters ---------- value: typing.Union[str, int] The value to parse (this argument can only be passed positionally). Returns ------- collections.abc.Iterator[hikari.Snowflake] An iterator over the role IDs in the string. """ _USER_ID_REGEX = re.compile(r"<@!?(\d+)>") parse_user_id: _IDMatcherSig = _make_snowflake_parser(_USER_ID_REGEX) """Parse a user ID from a string or int value. Parameters ---------- value: typing.Union[str, int] The value to parse (this argument can only be passed positionally). Other Parameters ---------------- message: str The error message to raise if the value cannot be parsed. Returns ------- hikari.Snowflake The parsed user ID. Raises ------ ValueError If the value cannot be parsed. """ search_user_ids: _IDSearcherSig = _make_snowflake_searcher(_USER_ID_REGEX) """Iterate over the user IDs in a string. Parameters ---------- value: typing.Union[str, int] The value to parse (this argument can only be passed positionally). Returns ------- collections.abc.Iterator[hikari.Snowflake] An iterator over the user IDs in the string. """ def _build_url_parser(callback: collections.Callable[[str], _ValueT], /) -> collections.Callable[[str], _ValueT]: def parse(value: str, /) -> _ValueT: """Convert an argument to a `urllib.parse` type. Parameters ---------- value: str The value to parse (this argument can only be passed positionally). Returns ------- _ValueT The parsed URL. Raises ------ ValueError If the argument couldn't be parsed. """ if value.startswith("<") and value.endswith(">"): value = value[1:-1] return callback(value) return parse defragment_url: collections.Callable[[str], urlparse.DefragResult] = _build_url_parser(urlparse.urldefrag) """Convert an argument to a defragmented URL. Parameters ---------- value: str The value to parse (this argument can only be passed positionally). Returns ------- urllib.parse.DefragResult The parsed URL. Raises ------ ValueError If the argument couldn't be parsed. """ parse_url: collections.Callable[[str], urlparse.ParseResult] = _build_url_parser(urlparse.urlparse) """Convert an argument to a parsed URL. Parameters ---------- value: str The value to parse (this argument can only be passed positionally). Returns ------- urllib.parse.ParseResult The parsed URL. Raises ------ ValueError If the argument couldn't be parsed. """ split_url: collections.Callable[[str], urlparse.SplitResult] = _build_url_parser(urlparse.urlsplit) """Convert an argument to a split URL. Parameters ---------- value: str The value to parse (this argument can only be passed positionally). Returns ------- urllib.parse.SplitResult The split URL. Raises ------ ValueError If the argument couldn't be parsed. """ _DATETIME_REGEX = re.compile(r"<-?t:(\d+)(?::\w)?>") def to_datetime(value: str, /) -> datetime.datetime: """Parse a datetime from Discord's datetime format. More information on this format can be found at https://discord.com/developers/docs/reference#message-formatting-timestamp-styles Parameters ---------- value: str The value to parse. Returns ------- datetime.datetime The parsed datetime. Raises ------ ValueError If the value cannot be parsed. """ try: timestamp = int(next(_DATETIME_REGEX.finditer(value)).groups()[0]) except StopIteration: raise ValueError("Not a valid datetime") from None return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc) _VALID_DATETIME_STYLES = frozenset(("t", "T", "d", "D", "f", "F", "R")) def from_datetime(value: datetime.datetime, /, *, style: str = "f") -> str: """Format a datetime as Discord's datetime format. More information on this format can be found at https://discord.com/developers/docs/reference#message-formatting-timestamp-styles Parameters ---------- value: datetime.datetime The datetime to format. Other Parameters ---------------- style: str The style to use. The valid styles can be found at https://discord.com/developers/docs/reference#message-formatting-formats and this defaults to `"f"`. Returns ------- str The formatted datetime. Raises ------ ValueError If the provided datetime is timezone naive. If an invalid style is provided. """ if style not in _VALID_DATETIME_STYLES: raise ValueError(f"Invalid style: {style}") if value.tzinfo is None: raise ValueError("Cannot convert naive datetimes, please specify a timezone.") return f"<t:{round(value.timestamp())}:{style}>" _YES_VALUES = frozenset(("y", "yes", "t", "true", "on", "1")) _NO_VALUES = frozenset(("n", "no", "f", "false", "off", "0")) def to_bool(value: str, /) -> bool: """Convert user string input into a boolean value. Parameters ---------- value: str The value to convert. Returns ------- bool The converted value. Raises ------ ValueError If the value cannot be converted. """ value = value.lower().strip() if value in _YES_VALUES: return True if value in _NO_VALUES: return False raise ValueError(f"Invalid bool value `{value}`") def to_color(argument: _ArgumentT, /) -> hikari.Color: """Convert user input to a `hikari.colors.Color` object.""" if isinstance(argument, str): values = argument.split(" ") if all(value.isdigit() for value in values): return hikari.Color.of(*map(int, values)) return hikari.Color.of(*values) return hikari.Color.of(argument) _TYPE_OVERRIDES: dict[collections.Callable[..., typing.Any], collections.Callable[[str], typing.Any]] = { bool: to_bool, bytes: lambda d: bytes(d, "utf-8"), bytearray: lambda d: bytearray(d, "utf-8"), datetime.datetime: to_datetime, hikari.Snowflake: parse_snowflake, urlparse.DefragResult: defragment_url, urlparse.ParseResult: parse_url, urlparse.SplitResult: split_url, } def override_type(cls: parsing.ConverterSig[typing.Any], /) -> parsing.ConverterSig[typing.Any]: return _TYPE_OVERRIDES.get(cls, cls) to_channel: typing.Final[ToChannel] = ToChannel() """Convert user input to a `hikari.channels.PartialChannel` object.""" to_colour: typing.Final[collections.Callable[[_ArgumentT], hikari.Color]] = to_color """Convert user input to a `hikari.colors.Color` object.""" to_emoji: typing.Final[ToEmoji] = ToEmoji() """Convert user input to a cached `hikari.emojis.KnownCustomEmoji` object. .. note:: If you just want to convert inpute to a `hikari.Emoji`, `hikari.CustomEmoji` or `hikari.UnicodeEmoji` without making any cache or REST calls then you can just use the relevant `hikari.Emoji.parse`, `hikari.CustomEmoji.parse` or `hikari.UnicodeEmoji.parse` methods. """ to_guild: typing.Final[ToGuild] = ToGuild() """Convert user input to a `hikari.guilds.Guild` object.""" to_invite: typing.Final[ToInvite] = ToInvite() """Convert user input to a cached `hikari.invites.InviteWithMetadata` object.""" to_invite_with_metadata: typing.Final[ToInviteWithMetadata] = ToInviteWithMetadata() """Convert user input to a `hikari.invites.Invite` object.""" to_member: typing.Final[ToMember] = ToMember() """Convert user input to a `hikari.guilds.Member` object.""" to_presence: typing.Final[ToPresence] = ToPresence() """Convert user input to a cached `hikari.presences.MemberPresence`.""" to_role: typing.Final[ToRole] = ToRole() """Convert user input to a `hikari.guilds.Role` object.""" to_snowflake: typing.Final[collections.Callable[[_ArgumentT], hikari.Snowflake]] = parse_snowflake """Convert user input to a `hikari.snowflakes.Snowflake`. .. note:: This also range validates the input. """ to_user: typing.Final[ToUser] = ToUser() """Convert user input to a `hikari.users.User` object.""" to_voice_state: typing.Final[ToVoiceState] = ToVoiceState() """Convert user input to a cached `hikari.voices.VoiceState`."""
Functions and classes used to enable more Discord oriented argument converters.
View Source
def to_bool(value: str, /) -> bool: """Convert user string input into a boolean value. Parameters ---------- value: str The value to convert. Returns ------- bool The converted value. Raises ------ ValueError If the value cannot be converted. """ value = value.lower().strip() if value in _YES_VALUES: return True if value in _NO_VALUES: return False raise ValueError(f"Invalid bool value `{value}`")
Convert user string input into a boolean value.
Parameters
- value (str): The value to convert.
Returns
- bool: The converted value.
Raises
- ValueError: If the value cannot be converted.
View Source
def to_color(argument: _ArgumentT, /) -> hikari.Color: """Convert user input to a `hikari.colors.Color` object.""" if isinstance(argument, str): values = argument.split(" ") if all(value.isdigit() for value in values): return hikari.Color.of(*map(int, values)) return hikari.Color.of(*values) return hikari.Color.of(argument)
Convert user input to a hikari.colors.Color object.
View Source
def to_color(argument: _ArgumentT, /) -> hikari.Color: """Convert user input to a `hikari.colors.Color` object.""" if isinstance(argument, str): values = argument.split(" ") if all(value.isdigit() for value in values): return hikari.Color.of(*map(int, values)) return hikari.Color.of(*values) return hikari.Color.of(argument)
Convert user input to a hikari.colors.Color object.
View Source
def to_datetime(value: str, /) -> datetime.datetime: """Parse a datetime from Discord's datetime format. More information on this format can be found at https://discord.com/developers/docs/reference#message-formatting-timestamp-styles Parameters ---------- value: str The value to parse. Returns ------- datetime.datetime The parsed datetime. Raises ------ ValueError If the value cannot be parsed. """ try: timestamp = int(next(_DATETIME_REGEX.finditer(value)).groups()[0]) except StopIteration: raise ValueError("Not a valid datetime") from None return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
Parse a datetime from Discord's datetime format.
More information on this format can be found at https://discord.com/developers/docs/reference#message-formatting-timestamp-styles
Parameters
- value (str): The value to parse.
Returns
- datetime.datetime: The parsed datetime.
Raises
- ValueError: If the value cannot be parsed.
View Source
def parse(value: _ArgumentT, /, *, message: str = "No valid mention or ID found") -> hikari.Snowflake: """Parse a snowflake from a string or int value. .. note:: This only allows the relevant entity's mention format if applicable. Parameters ---------- value: typing.Union[str, int] The value to parse (this argument can only be passed positionally). Other Parameters ---------------- message: str The error message to raise if the value cannot be parsed. Returns ------- hikari.Snowflake The parsed snowflake. Raises ------ ValueError If the value cannot be parsed. """ result: typing.Optional[hikari.Snowflake] = None if isinstance(value, str): if value.isdigit(): result = hikari.Snowflake(value) else: capture = next(regex.finditer(value), None) result = hikari.Snowflake(capture.groups()[0]) if capture else None else: try: # Technically passing a float here is invalid (typing wise) # but we handle that by catching TypeError result = hikari.Snowflake(operator.index(typing.cast(int, value))) except (TypeError, ValueError): pass # We should also range check the provided ID. if result is not None and _range_check(result): return result raise ValueError(message) from None
Parse a snowflake from a string or int value.
Note: This only allows the relevant entity's mention format if applicable.
Parameters
- value (typing.Union[str, int]): The value to parse (this argument can only be passed positionally).
Other Parameters
- message (str): The error message to raise if the value cannot be parsed.
Returns
- hikari.Snowflake: The parsed snowflake.
Raises
- ValueError: If the value cannot be parsed.
View Source
# -*- coding: utf-8 -*- # cython: language_level=3 # BSD 3-Clause License # # Copyright (c) 2020-2022, Faster Speeding # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Default dependency utilities used within Tanjun and their abstract interfaces.""" from __future__ import annotations __all__: list[str] = [ # __init__.py "set_standard_dependencies", # async_cache.py "async_cache", "AsyncCache", "ChannelBoundCache", "CacheIterator", "CacheMissError", "EntryNotFound", "GuildBoundCache", "SingleStoreCache", "SfCache", "SfChannelBound", "SfGuildBound", # callbacks.py "callbacks", "fetch_my_user", # data.py "data", "cache_callback", "cached_inject", "LazyConstant", "inject_lc", "make_lc_resolver", # limiters.py "limiters", "AbstractConcurrencyLimiter", "AbstractCooldownManager", "BucketResource", "ConcurrencyPreExecution", "ConcurrencyPostExecution", "CooldownPreExecution", "InMemoryConcurrencyLimiter", "InMemoryCooldownManager", "with_concurrency_limit", "with_cooldown", # owners.py "owners", "AbstractOwners", "Owners", ] import hikari from .. import injecting from .async_cache import AsyncCache from .async_cache import CacheIterator from .async_cache import CacheMissError from .async_cache import ChannelBoundCache from .async_cache import EntryNotFound from .async_cache import GuildBoundCache from .async_cache import SfCache from .async_cache import SfChannelBound from .async_cache import SfGuildBound from .async_cache import SingleStoreCache from .callbacks import fetch_my_user from .data import LazyConstant from .data import cache_callback from .data import cached_inject from .data import inject_lc from .data import make_lc_resolver from .limiters import AbstractConcurrencyLimiter from .limiters import AbstractCooldownManager from .limiters import BucketResource from .limiters import ConcurrencyPostExecution from .limiters import ConcurrencyPreExecution from .limiters import CooldownPreExecution from .limiters import InMemoryConcurrencyLimiter from .limiters import InMemoryCooldownManager from .limiters import with_concurrency_limit from .limiters import with_cooldown from .owners import AbstractOwners from .owners import Owners def set_standard_dependencies(client: injecting.InjectorClient, /) -> None: """Set the standard dependencies for Tanjun. Parameters ---------- client: tanjun.injecting.InjectorClient The injector client to set the standard dependencies on. """ client.set_type_dependency(AbstractOwners, Owners()).set_type_dependency( LazyConstant[hikari.OwnUser], LazyConstant[hikari.OwnUser](fetch_my_user) )
Default dependency utilities used within Tanjun and their abstract interfaces.
View Source
class BucketResource(int, enum.Enum): """Resource target types used within command calldowns and concurrency limiters.""" USER = 0 """A per-user resource bucket.""" MEMBER = 1 """A per-guild member resource bucket. .. note:: When executed in a DM this will be per-DM. """ CHANNEL = 2 """A per-channel resource bucket.""" PARENT_CHANNEL = 3 """A per-parent channel resource bucket. .. note:: For DM channels this will be per-DM, for guild channels with no parents this'll be per-guild. """ # CATEGORY = 4 # """A per-category resource bucket. # .. note:: # For DM channels this will be per-DM, for guild channels with no parent # category this'll be per-guild. # """ TOP_ROLE = 5 """A per-highest role resource bucket. .. note:: When executed in a DM this will be per-DM, with this defaulting to targeting the @everyone role if they have no real roles. """ GUILD = 6 """A per-guild resource bucket. .. note:: When executed in a DM this will be per-DM. """ GLOBAL = 7 """A global resource bucket."""
Resource target types used within command calldowns and concurrency limiters.
A per-user resource bucket.
A per-guild member resource bucket.
Note: When executed in a DM this will be per-DM.
A per-channel resource bucket.
A per-parent channel resource bucket.
Note: For DM channels this will be per-DM, for guild channels with no parents this'll be per-guild.
A per-highest role resource bucket.
Note: When executed in a DM this will be per-DM, with this defaulting to targeting the @everyone role if they have no real roles.
A per-guild resource bucket.
Note: When executed in a DM this will be per-DM.
A global resource bucket.
Inherited Members
- enum.Enum
- name
- value
- builtins.int
- conjugate
- bit_length
- to_bytes
- from_bytes
- as_integer_ratio
- real
- imag
- numerator
- denominator
View Source
def cached_inject( callback: injecting.CallbackSig[_T], /, *, expire_after: typing.Union[float, int, datetime.timedelta, None] = None ) -> _T: """Inject a callback with caching. This acts like `tanjun.injecting.inject` and the result of it should also be assigned to a parameter's default to be used. Example ------- ```py async def resolve_database( client: tanjun.abc.Client = tanjun.inject(type=tanjun.abc.Client) ) -> Database: raise NotImplementedError @tanjun.as_message_command("command name") async def command( ctx: tanjun.abc.Context, db: Database = tanjun.cached_inject(resolve_database) ) -> None: raise NotImplementedError Parameters ---------- callback : CallbackSig[_T] The callback to inject. Other Parameters ---------------- expire_after : typing.Union[int, float, datetime.timedelta, None] The amount of time to cache the result for in seconds. Leave this as `None` to cache for the runtime of the application. Returns ------- tanjun.injecting.Injected[_T] Injector used to resolve the cached callback. Raises ------ ValueError If expire_after is not a valid value. If expire_after is not less than or equal to 0 seconds. """ return injecting.inject(callback=cache_callback(callback, expire_after=expire_after))
Inject a callback with caching.
This acts like tanjun.injecting.inject and the result of it
should also be assigned to a parameter's default to be used.
Example
```py async def resolve_database( client: tanjun.abc.Client = tanjun.inject(type=tanjun.abc.Client) ) -> Database: raise NotImplementedError
@tanjun.as_message_command("command name") async def command( ctx: tanjun.abc.Context, db: Database = tanjun.cached_inject(resolve_database) ) -> None: raise NotImplementedError
Parameters
- callback (CallbackSig[_T]): The callback to inject.
Other Parameters
expire_after (typing.Union[int, float, datetime.timedelta, None]): The amount of time to cache the result for in seconds.
Leave this as
Noneto cache for the runtime of the application.
Returns
- tanjun.injecting.Injected[_T]: Injector used to resolve the cached callback.
Raises
- ValueError: If expire_after is not a valid value. If expire_after is not less than or equal to 0 seconds.
View Source
def inject_lc(type_: type[_T], /) -> _T: """Make a LazyConstant injector. This acts like `tanjun.injecting.inject` and the result of it should also be assigned to a parameter's default to be used. .. note:: For this to work, a `LazyConstant` must've been set as a type dependency for the passed `type_`. Parameters ---------- type_ : type[_T] The type of the constant to resolve. Returns ------- tanjun.injecting.Injected[_T] Injector used to resolve the LazyConstant. Example ------- ```py @component.with_command @tanjun.as_message_command async def command( ctx: tanjun.abc.MessageCommand, application: hikari.Application = tanjun.inject_lc(hikari.Application) ) -> None: raise NotImplementedError ... async def resolve_app( client: tanjun.abc.Client = tanjun.inject(type=tanjun.abc.Client) ) -> hikari.Application: raise NotImplementedError tanjun.Client.from_gateway_bot(...).set_type_dependency( tanjun.LazyConstant[hikari.Application] = tanjun.LazyConstant(resolve_app) ) ``` """ return injecting.inject(callback=make_lc_resolver(type_))
Make a LazyConstant injector.
This acts like tanjun.injecting.inject and the result of it
should also be assigned to a parameter's default to be used.
Note:
For this to work, a LazyConstant must've been set as a type
dependency for the passed type_.
Parameters
- type_ (type[_T]): The type of the constant to resolve.
Returns
- tanjun.injecting.Injected[_T]: Injector used to resolve the LazyConstant.
Example
@component.with_command
@tanjun.as_message_command
async def command(
ctx: tanjun.abc.MessageCommand,
application: hikari.Application = tanjun.inject_lc(hikari.Application)
) -> None:
raise NotImplementedError
...
async def resolve_app(
client: tanjun.abc.Client = tanjun.inject(type=tanjun.abc.Client)
) -> hikari.Application:
raise NotImplementedError
tanjun.Client.from_gateway_bot(...).set_type_dependency(
tanjun.LazyConstant[hikari.Application] = tanjun.LazyConstant(resolve_app)
)
View Source
class InMemoryConcurrencyLimiter(AbstractConcurrencyLimiter): """In-memory standard implementation of `AbstractConcurrencyLimiter`. Examples -------- `InMemoryConcurrencyLimiter.set_bucket` may be used to set the concurrency limits for a specific bucket: ```py ( InMemoryConcurrencyLimiter() # Set the default bucket template to 10 concurrent uses of the command per-user. .set_bucket("default", tanjun.BucketResource.USER, 10) # Set the "moderation" bucket with a limit of 5 concurrent uses per-guild. .set_bucket("moderation", tanjun.BucketResource.GUILD, 5) .set_bucket() # add_to_client will setup the concurrency manager (setting it as an # injected dependency and registering callbacks to manage it). .add_to_client(client) ) ``` """ __slots__ = ("_acquiring_ctxs", "_buckets", "_default_bucket_template", "_gc_task") def __init__(self) -> None: self._acquiring_ctxs: dict[tuple[str, tanjun_abc.Context], _ConcurrencyLimit] = {} self._buckets: dict[str, _BaseResource[_ConcurrencyLimit]] = {} self._default_bucket_template: _BaseResource[_ConcurrencyLimit] = _FlatResource( BucketResource.USER, lambda: _ConcurrencyLimit(limit=1) ) self._gc_task: typing.Optional[asyncio.Task[None]] = None async def _gc(self) -> None: while True: await asyncio.sleep(10) for bucket in self._buckets.values(): bucket.cleanup() def add_to_client(self, client: injecting.InjectorClient, /) -> None: """Add this concurrency manager to a tanjun client. .. note:: This registers the manager as a type dependency and manages opening and closing the manager based on the client's life cycle. Parameters ---------- client : tanjun.abc.Client The client to add this concurrency manager to. """ client.set_type_dependency(AbstractConcurrencyLimiter, self) # TODO: the injection client should be upgraded to the abstract Client. assert isinstance(client, tanjun_abc.Client) client.add_client_callback(tanjun_abc.ClientCallbackNames.STARTING, self.open) client.add_client_callback(tanjun_abc.ClientCallbackNames.CLOSING, self.close) if client.is_alive: assert client.loop is not None self.open(_loop=client.loop) def close(self) -> None: """Stop the concurrency manager. Raises ------ RuntimeError If the concurrency manager is not running. """ if not self._gc_task: raise RuntimeError("Concurrency manager is not active") self._gc_task.cancel() self._gc_task = None def open(self, *, _loop: typing.Optional[asyncio.AbstractEventLoop] = None) -> None: """Start the concurrency manager. Raises ------ RuntimeError If the concurrency manager is already running. If called in a thread with no running event loop. """ if self._gc_task: raise RuntimeError("Concurrency manager is already running") self._gc_task = (_loop or asyncio.get_running_loop()).create_task(self._gc()) async def try_acquire(self, bucket_id: str, ctx: tanjun_abc.Context, /) -> bool: # <<inherited docstring from AbstractConcurrencyLimiter>>. bucket = self._buckets.get(bucket_id) if not bucket: _LOGGER.info("No concurrency limit found for %r, falling back to 'default' bucket", bucket_id) bucket = self._buckets[bucket_id] = self._default_bucket_template.copy() # incrementing a bucket multiple times for the same context could lead # to weird edge cases based on how we internally track this, so we # internally de-duplicate this. elif (bucket_id, ctx) in self._acquiring_ctxs: return True # This won't ever be the case if it just had to make a new bucket, hence the elif. if result := (limit := await bucket.into_inner(ctx)).acquire(): self._acquiring_ctxs[(bucket_id, ctx)] = limit return result async def release(self, bucket_id: str, ctx: tanjun_abc.Context, /) -> None: # <<inherited docstring from AbstractConcurrencyLimiter>>. if limit := self._acquiring_ctxs.pop((bucket_id, ctx), None): limit.release() def disable_bucket(self: _InMemoryConcurrencyLimiterT, bucket_id: str, /) -> _InMemoryConcurrencyLimiterT: """Disable a concurrency limit bucket. This will stop the bucket from ever hitting a concurrency limit and also prevents the bucket from defaulting. Parameters ---------- bucket_id : str The bucket to disable. .. note:: "default" is a special bucket which is used as a template for unknown bucket IDs. Returns ------- Self This concurrency manager to allow for chaining. """ bucket = self._buckets[bucket_id] = _GlobalResource(lambda: _ConcurrencyLimit(limit=-1)) if bucket_id == "default": self._default_bucket_template = bucket.copy() return self def set_bucket( self: _InMemoryConcurrencyLimiterT, bucket_id: str, resource: BucketResource, limit: int, / ) -> _InMemoryConcurrencyLimiterT: """Set the concurrency limit for a specific bucket. Parameters ---------- bucket_id : str The ID of the bucket to set the concurrency limit for. .. note:: "default" is a special bucket which is used as a template for unknown bucket IDs. resource : tanjun.BucketResource The type of resource to target for the concurrency limit. limit : int The maximum number of concurrent uses to allow. Returns ------- Self The concurrency manager to allow call chaining. Raises ------ ValueError If an invalid resource type is given. if limit is less 0 or negative. """ if limit <= 0: raise ValueError("limit must be greater than 0") bucket = self._buckets[bucket_id] = _to_bucket(BucketResource(resource), lambda: _ConcurrencyLimit(limit=limit)) if bucket_id == "default": self._default_bucket_template = bucket.copy() return self
In-memory standard implementation of AbstractConcurrencyLimiter.
Examples
InMemoryConcurrencyLimiter.set_bucket may be used to set the concurrency
limits for a specific bucket:
(
InMemoryConcurrencyLimiter()
# Set the default bucket template to 10 concurrent uses of the command per-user.
.set_bucket("default", tanjun.BucketResource.USER, 10)
# Set the "moderation" bucket with a limit of 5 concurrent uses per-guild.
.set_bucket("moderation", tanjun.BucketResource.GUILD, 5)
.set_bucket()
# add_to_client will setup the concurrency manager (setting it as an
# injected dependency and registering callbacks to manage it).
.add_to_client(client)
)
View Source
def __init__(self) -> None: self._acquiring_ctxs: dict[tuple[str, tanjun_abc.Context], _ConcurrencyLimit] = {} self._buckets: dict[str, _BaseResource[_ConcurrencyLimit]] = {} self._default_bucket_template: _BaseResource[_ConcurrencyLimit] = _FlatResource( BucketResource.USER, lambda: _ConcurrencyLimit(limit=1) ) self._gc_task: typing.Optional[asyncio.Task[None]] = None
View Source
def add_to_client(self, client: injecting.InjectorClient, /) -> None: """Add this concurrency manager to a tanjun client. .. note:: This registers the manager as a type dependency and manages opening and closing the manager based on the client's life cycle. Parameters ---------- client : tanjun.abc.Client The client to add this concurrency manager to. """ client.set_type_dependency(AbstractConcurrencyLimiter, self) # TODO: the injection client should be upgraded to the abstract Client. assert isinstance(client, tanjun_abc.Client) client.add_client_callback(tanjun_abc.ClientCallbackNames.STARTING, self.open) client.add_client_callback(tanjun_abc.ClientCallbackNames.CLOSING, self.close) if client.is_alive: assert client.loop is not None self.open(_loop=client.loop)
Add this concurrency manager to a tanjun client.
Note: This registers the manager as a type dependency and manages opening and closing the manager based on the client's life cycle.
Parameters
- client (tanjun.abc.Client): The client to add this concurrency manager to.
View Source
def close(self) -> None: """Stop the concurrency manager. Raises ------ RuntimeError If the concurrency manager is not running. """ if not self._gc_task: raise RuntimeError("Concurrency manager is not active") self._gc_task.cancel() self._gc_task = None
Stop the concurrency manager.
Raises
- RuntimeError: If the concurrency manager is not running.
View Source
def open(self, *, _loop: typing.Optional[asyncio.AbstractEventLoop] = None) -> None: """Start the concurrency manager. Raises ------ RuntimeError If the concurrency manager is already running. If called in a thread with no running event loop. """ if self._gc_task: raise RuntimeError("Concurrency manager is already running") self._gc_task = (_loop or asyncio.get_running_loop()).create_task(self._gc())
Start the concurrency manager.
Raises
- RuntimeError: If the concurrency manager is already running. If called in a thread with no running event loop.
View Source
async def try_acquire(self, bucket_id: str, ctx: tanjun_abc.Context, /) -> bool: # <<inherited docstring from AbstractConcurrencyLimiter>>. bucket = self._buckets.get(bucket_id) if not bucket: _LOGGER.info("No concurrency limit found for %r, falling back to 'default' bucket", bucket_id) bucket = self._buckets[bucket_id] = self._default_bucket_template.copy() # incrementing a bucket multiple times for the same context could lead # to weird edge cases based on how we internally track this, so we # internally de-duplicate this. elif (bucket_id, ctx) in self._acquiring_ctxs: return True # This won't ever be the case if it just had to make a new bucket, hence the elif. if result := (limit := await bucket.into_inner(ctx)).acquire(): self._acquiring_ctxs[(bucket_id, ctx)] = limit return result
Try to acquire a concurrency lock on a bucket.
Parameters
- bucket_id (str): The concurrency bucket to acquire.
- ctx (tanjun.abc.Context): The context to acquire this resource lock with.
Returns
- bool: Whether the lock was acquired.
View Source
async def release(self, bucket_id: str, ctx: tanjun_abc.Context, /) -> None: # <<inherited docstring from AbstractConcurrencyLimiter>>. if limit := self._acquiring_ctxs.pop((bucket_id, ctx), None): limit.release()
Release a concurrency lock on a bucket.
View Source
def disable_bucket(self: _InMemoryConcurrencyLimiterT, bucket_id: str, /) -> _InMemoryConcurrencyLimiterT: """Disable a concurrency limit bucket. This will stop the bucket from ever hitting a concurrency limit and also prevents the bucket from defaulting. Parameters ---------- bucket_id : str The bucket to disable. .. note:: "default" is a special bucket which is used as a template for unknown bucket IDs. Returns ------- Self This concurrency manager to allow for chaining. """ bucket = self._buckets[bucket_id] = _GlobalResource(lambda: _ConcurrencyLimit(limit=-1)) if bucket_id == "default": self._default_bucket_template = bucket.copy() return self
Disable a concurrency limit bucket.
This will stop the bucket from ever hitting a concurrency limit and also prevents the bucket from defaulting.
Parameters
bucket_id (str): The bucket to disable.
Note: "default" is a special bucket which is used as a template for unknown bucket IDs.
Returns
- Self: This concurrency manager to allow for chaining.
View Source
def set_bucket( self: _InMemoryConcurrencyLimiterT, bucket_id: str, resource: BucketResource, limit: int, / ) -> _InMemoryConcurrencyLimiterT: """Set the concurrency limit for a specific bucket. Parameters ---------- bucket_id : str The ID of the bucket to set the concurrency limit for. .. note:: "default" is a special bucket which is used as a template for unknown bucket IDs. resource : tanjun.BucketResource The type of resource to target for the concurrency limit. limit : int The maximum number of concurrent uses to allow. Returns ------- Self The concurrency manager to allow call chaining. Raises ------ ValueError If an invalid resource type is given. if limit is less 0 or negative. """ if limit <= 0: raise ValueError("limit must be greater than 0") bucket = self._buckets[bucket_id] = _to_bucket(BucketResource(resource), lambda: _ConcurrencyLimit(limit=limit)) if bucket_id == "default": self._default_bucket_template = bucket.copy() return self
Set the concurrency limit for a specific bucket.
Parameters
bucket_id (str): The ID of the bucket to set the concurrency limit for.
Note: "default" is a special bucket which is used as a template for unknown bucket IDs.
- resource (tanjun.BucketResource): The type of resource to target for the concurrency limit.
- limit (int): The maximum number of concurrent uses to allow.
Returns
- Self: The concurrency manager to allow call chaining.
Raises
- ValueError: If an invalid resource type is given. if limit is less 0 or negative.
View Source
class InMemoryCooldownManager(AbstractCooldownManager): """In-memory standard implementation of `AbstractCooldownManager`. Examples -------- `InMemoryCooldownManager.set_bucket` may be used to set the cooldown for a specific bucket: ```py ( InMemoryCooldownManager() # Set the default bucket template to a per-user 10 uses per-60 seconds cooldown. .set_bucket("default", tanjun.BucketResource.USER, 10, 60) # Set the "moderation" bucket to a per-guild 100 uses per-5 minutes cooldown. .set_bucket("moderation", tanjun.BucketResource.GUILD, 100, datetime.timedelta(minutes=5)) .set_bucket() # add_to_client will setup the cooldown manager (setting it as an # injected dependency and registering callbacks to manage it). .add_to_client(client) ) ``` """ __slots__ = ("_buckets", "_default_bucket_template", "_gc_task") def __init__(self) -> None: self._buckets: dict[str, _BaseResource[_Cooldown]] = {} self._default_bucket_template: _BaseResource[_Cooldown] = _FlatResource( BucketResource.USER, lambda: _Cooldown(limit=2, reset_after=5) ) self._gc_task: typing.Optional[asyncio.Task[None]] = None def _get_or_default(self, bucket_id: str, /) -> _BaseResource[_Cooldown]: if bucket := self._buckets.get(bucket_id): return bucket _LOGGER.info("No cooldown found for %r, falling back to 'default' bucket", bucket_id) bucket = self._buckets[bucket_id] = self._default_bucket_template.copy() return bucket async def _gc(self) -> None: while True: await asyncio.sleep(10) for bucket in self._buckets.values(): bucket.cleanup() def add_to_client(self, client: injecting.InjectorClient, /) -> None: """Add this cooldown manager to a tanjun client. .. note:: This registers the manager as a type dependency and manages opening and closing the manager based on the client's life cycle. Parameters ---------- client : tanjun.abc.Client The client to add this cooldown manager to. """ client.set_type_dependency(AbstractCooldownManager, self) # TODO: the injection client should be upgraded to the abstract Client. assert isinstance(client, tanjun_abc.Client) client.add_client_callback(tanjun_abc.ClientCallbackNames.STARTING, self.open) client.add_client_callback(tanjun_abc.ClientCallbackNames.CLOSING, self.close) if client.is_alive: assert client.loop is not None self.open(_loop=client.loop) async def check_cooldown( self, bucket_id: str, ctx: tanjun_abc.Context, /, *, increment: bool = False ) -> typing.Optional[float]: # <<inherited docstring from AbstractCooldownManager>>. if increment: bucket = await self._get_or_default(bucket_id).into_inner(ctx) if cooldown := bucket.must_wait_for(): return cooldown bucket.increment() return None if (bucket := self._buckets.get(bucket_id)) and (cooldown := await bucket.try_into_inner(ctx)): return cooldown.must_wait_for() async def increment_cooldown(self, bucket_id: str, ctx: tanjun_abc.Context, /) -> None: # <<inherited docstring from AbstractCooldownManager>>. (await self._get_or_default(bucket_id).into_inner(ctx)).increment() def close(self) -> None: """Stop the cooldown manager. Raises ------ RuntimeError If the cooldown manager is not running. """ if not self._gc_task: raise RuntimeError("Cooldown manager is not active") self._gc_task.cancel() self._gc_task = None def open(self, *, _loop: typing.Optional[asyncio.AbstractEventLoop] = None) -> None: """Start the cooldown manager. Raises ------ RuntimeError If the cooldown manager is already running. If called in a thread with no running event loop. """ if self._gc_task: raise RuntimeError("Cooldown manager is already running") self._gc_task = (_loop or asyncio.get_running_loop()).create_task(self._gc()) def disable_bucket(self: _InMemoryCooldownManagerT, bucket_id: str, /) -> _InMemoryCooldownManagerT: """Disable a cooldown bucket. This will stop the bucket from ever hitting a cooldown and also prevents the bucket from defaulting. Parameters ---------- bucket_id : str The bucket to disable. .. note:: "default" is a special bucket which is used as a template for unknown bucket IDs. Returns ------- Self This cooldown manager to allow for chaining. """ # A limit of -1 is special cased to mean no limit and reset_after is ignored in this scenario. bucket = self._buckets[bucket_id] = _GlobalResource(lambda: _Cooldown(limit=-1, reset_after=-1)) if bucket_id == "default": self._default_bucket_template = bucket.copy() return self def set_bucket( self: _InMemoryCooldownManagerT, bucket_id: str, resource: BucketResource, limit: int, reset_after: typing.Union[int, float, datetime.timedelta], /, ) -> _InMemoryCooldownManagerT: """Set the cooldown for a specific bucket. Parameters ---------- bucket_id : str The ID of the bucket to set the cooldown for. .. note:: "default" is a special bucket which is used as a template for unknown bucket IDs. resource : tanjun.BucketResource The type of resource to target for the cooldown. limit : int The number of uses per cooldown period. reset_after : int, float, datetime.timedelta The cooldown period. Returns ------- Self The cooldown manager to allow call chaining. Raises ------ ValueError If an invalid resource type is given. If reset_after or limit are negative, 0 or invalid. if limit is less 0 or negative. """ if isinstance(reset_after, datetime.timedelta): reset_after_seconds = reset_after.total_seconds() else: reset_after_seconds = float(reset_after) if reset_after_seconds <= 0: raise ValueError("reset_after must be greater than 0 seconds") if limit <= 0: raise ValueError("limit must be greater than 0") bucket = self._buckets[bucket_id] = _to_bucket( BucketResource(resource), lambda: _Cooldown(limit=limit, reset_after=reset_after_seconds) ) if bucket_id == "default": self._default_bucket_template = bucket.copy() return self
In-memory standard implementation of AbstractCooldownManager.
Examples
InMemoryCooldownManager.set_bucket may be used to set the cooldown for a
specific bucket:
(
InMemoryCooldownManager()
# Set the default bucket template to a per-user 10 uses per-60 seconds cooldown.
.set_bucket("default", tanjun.BucketResource.USER, 10, 60)
# Set the "moderation" bucket to a per-guild 100 uses per-5 minutes cooldown.
.set_bucket("moderation", tanjun.BucketResource.GUILD, 100, datetime.timedelta(minutes=5))
.set_bucket()
# add_to_client will setup the cooldown manager (setting it as an
# injected dependency and registering callbacks to manage it).
.add_to_client(client)
)
View Source
def __init__(self) -> None: self._buckets: dict[str, _BaseResource[_Cooldown]] = {} self._default_bucket_template: _BaseResource[_Cooldown] = _FlatResource( BucketResource.USER, lambda: _Cooldown(limit=2, reset_after=5) ) self._gc_task: typing.Optional[asyncio.Task[None]] = None
View Source
def add_to_client(self, client: injecting.InjectorClient, /) -> None: """Add this cooldown manager to a tanjun client. .. note:: This registers the manager as a type dependency and manages opening and closing the manager based on the client's life cycle. Parameters ---------- client : tanjun.abc.Client The client to add this cooldown manager to. """ client.set_type_dependency(AbstractCooldownManager, self) # TODO: the injection client should be upgraded to the abstract Client. assert isinstance(client, tanjun_abc.Client) client.add_client_callback(tanjun_abc.ClientCallbackNames.STARTING, self.open) client.add_client_callback(tanjun_abc.ClientCallbackNames.CLOSING, self.close) if client.is_alive: assert client.loop is not None self.open(_loop=client.loop)
Add this cooldown manager to a tanjun client.
Note: This registers the manager as a type dependency and manages opening and closing the manager based on the client's life cycle.
Parameters
- client (tanjun.abc.Client): The client to add this cooldown manager to.
View Source
async def check_cooldown( self, bucket_id: str, ctx: tanjun_abc.Context, /, *, increment: bool = False ) -> typing.Optional[float]: # <<inherited docstring from AbstractCooldownManager>>. if increment: bucket = await self._get_or_default(bucket_id).into_inner(ctx) if cooldown := bucket.must_wait_for(): return cooldown bucket.increment() return None if (bucket := self._buckets.get(bucket_id)) and (cooldown := await bucket.try_into_inner(ctx)): return cooldown.must_wait_for()
Check if a bucket is on cooldown for the provided context.
Parameters
- bucket_id (str): The cooldown bucket to check.
- ctx (tanjun.abc.Context): The context of the command.
Other Parameters
- increment (bool): Whether this call should increment the bucket's use counter if it isn't depleted.
Returns
- typing.Optional[float]: When this command will next be usable for the provided context
if it's in cooldown else
None.
View Source
async def increment_cooldown(self, bucket_id: str, ctx: tanjun_abc.Context, /) -> None: # <<inherited docstring from AbstractCooldownManager>>. (await self._get_or_default(bucket_id).into_inner(ctx)).increment()
Increment the cooldown of a cooldown bucket.
Parameters
- bucket_id (str): The cooldown bucket's ID.
- ctx (tanjun.abc.Context): The context of the command.
View Source
def close(self) -> None: """Stop the cooldown manager. Raises ------ RuntimeError If the cooldown manager is not running. """ if not self._gc_task: raise RuntimeError("Cooldown manager is not active") self._gc_task.cancel() self._gc_task = None
Stop the cooldown manager.
Raises
- RuntimeError: If the cooldown manager is not running.
View Source
def open(self, *, _loop: typing.Optional[asyncio.AbstractEventLoop] = None) -> None: """Start the cooldown manager. Raises ------ RuntimeError If the cooldown manager is already running. If called in a thread with no running event loop. """ if self._gc_task: raise RuntimeError("Cooldown manager is already running") self._gc_task = (_loop or asyncio.get_running_loop()).create_task(self._gc())
Start the cooldown manager.
Raises
- RuntimeError: If the cooldown manager is already running. If called in a thread with no running event loop.
View Source
def disable_bucket(self: _InMemoryCooldownManagerT, bucket_id: str, /) -> _InMemoryCooldownManagerT: """Disable a cooldown bucket. This will stop the bucket from ever hitting a cooldown and also prevents the bucket from defaulting. Parameters ---------- bucket_id : str The bucket to disable. .. note:: "default" is a special bucket which is used as a template for unknown bucket IDs. Returns ------- Self This cooldown manager to allow for chaining. """ # A limit of -1 is special cased to mean no limit and reset_after is ignored in this scenario. bucket = self._buckets[bucket_id] = _GlobalResource(lambda: _Cooldown(limit=-1, reset_after=-1)) if bucket_id == "default": self._default_bucket_template = bucket.copy() return self
Disable a cooldown bucket.
This will stop the bucket from ever hitting a cooldown and also prevents the bucket from defaulting.
Parameters
bucket_id (str): The bucket to disable.
Note: "default" is a special bucket which is used as a template for unknown bucket IDs.
Returns
- Self: This cooldown manager to allow for chaining.
View Source
def set_bucket( self: _InMemoryCooldownManagerT, bucket_id: str, resource: BucketResource, limit: int, reset_after: typing.Union[int, float, datetime.timedelta], /, ) -> _InMemoryCooldownManagerT: """Set the cooldown for a specific bucket. Parameters ---------- bucket_id : str The ID of the bucket to set the cooldown for. .. note:: "default" is a special bucket which is used as a template for unknown bucket IDs. resource : tanjun.BucketResource The type of resource to target for the cooldown. limit : int The number of uses per cooldown period. reset_after : int, float, datetime.timedelta The cooldown period. Returns ------- Self The cooldown manager to allow call chaining. Raises ------ ValueError If an invalid resource type is given. If reset_after or limit are negative, 0 or invalid. if limit is less 0 or negative. """ if isinstance(reset_after, datetime.timedelta): reset_after_seconds = reset_after.total_seconds() else: reset_after_seconds = float(reset_after) if reset_after_seconds <= 0: raise ValueError("reset_after must be greater than 0 seconds") if limit <= 0: raise ValueError("limit must be greater than 0") bucket = self._buckets[bucket_id] = _to_bucket( BucketResource(resource), lambda: _Cooldown(limit=limit, reset_after=reset_after_seconds) ) if bucket_id == "default": self._default_bucket_template = bucket.copy() return self
Set the cooldown for a specific bucket.
Parameters
bucket_id (str): The ID of the bucket to set the cooldown for.
Note: "default" is a special bucket which is used as a template for unknown bucket IDs.
- resource (tanjun.BucketResource): The type of resource to target for the cooldown.
- limit (int): The number of uses per cooldown period.
- reset_after (int, float, datetime.timedelta): The cooldown period.
Returns
- Self: The cooldown manager to allow call chaining.
Raises
- ValueError: If an invalid resource type is given. If reset_after or limit are negative, 0 or invalid. if limit is less 0 or negative.
View Source
class LazyConstant(typing.Generic[_T]): """Injected type used to hold and generate lazy constants. .. note:: To easily resolve this type use `inject_lc`. """ __slots__ = ("_callback", "_lock", "_value") def __init__(self, callback: collections.Callable[..., tanjun_abc.MaybeAwaitableT[_T]], /) -> None: """Initiate a new lazy constant. Parameters ---------- callback : collections.abc.Callable[..., tanjun.abc.MaybeAwaitable[_T]] Callback used to resolve this to a constant value. This supports dependency injection and may either be sync or asynchronous. """ self._callback = injecting.CallbackDescriptor(callback) self._lock: typing.Optional[asyncio.Lock] = None self._value: typing.Optional[_T] = None @property def callback(self) -> injecting.CallbackDescriptor[_T]: """Descriptor of the callback used to get this constant's initial value.""" return self._callback def get_value(self) -> typing.Optional[_T]: """Get the value of this constant if set, else `None`.""" return self._value def reset(self: _LazyConstantT) -> _LazyConstantT: """Clear the internally stored value.""" self._value = None return self def set_value(self: _LazyConstantT, value: _T, /) -> _LazyConstantT: """Set the constant value. Parameters ---------- value : _T The value to set. Raises ------ RuntimeError If the constant has already been set. """ if self._value is not None: raise RuntimeError("Constant value already set.") self._value = value self._lock = None return self def acquire(self) -> contextlib.AbstractAsyncContextManager[typing.Any]: """Acquire this lazy constant as an asynchronous lock. This is used to ensure that the value is only generated once and should be kept acquired until `LazyConstant.set_value` has been called. Returns ------- contextlib.AbstractAsyncContextManager[typing.Any] Context manager that can be used to acquire the lock. """ if not self._lock: # Error if this is called outside of a running event loop. asyncio.get_running_loop() self._lock = asyncio.Lock() return self._lock
Injected type used to hold and generate lazy constants.
Note:
To easily resolve this type use inject_lc.
View Source
def __init__(self, callback: collections.Callable[..., tanjun_abc.MaybeAwaitableT[_T]], /) -> None: """Initiate a new lazy constant. Parameters ---------- callback : collections.abc.Callable[..., tanjun.abc.MaybeAwaitable[_T]] Callback used to resolve this to a constant value. This supports dependency injection and may either be sync or asynchronous. """ self._callback = injecting.CallbackDescriptor(callback) self._lock: typing.Optional[asyncio.Lock] = None self._value: typing.Optional[_T] = None
Initiate a new lazy constant.
Parameters
callback (collections.abc.Callable[..., tanjun.abc.MaybeAwaitable[_T]]): Callback used to resolve this to a constant value.
This supports dependency injection and may either be sync or asynchronous.
Descriptor of the callback used to get this constant's initial value.
View Source
def get_value(self) -> typing.Optional[_T]: """Get the value of this constant if set, else `None`.""" return self._value
Get the value of this constant if set, else None.
View Source
def reset(self: _LazyConstantT) -> _LazyConstantT: """Clear the internally stored value.""" self._value = None return self
Clear the internally stored value.
View Source
def set_value(self: _LazyConstantT, value: _T, /) -> _LazyConstantT: """Set the constant value. Parameters ---------- value : _T The value to set. Raises ------ RuntimeError If the constant has already been set. """ if self._value is not None: raise RuntimeError("Constant value already set.") self._value = value self._lock = None return self
Set the constant value.
Parameters
- value (_T): The value to set.
Raises
- RuntimeError: If the constant has already been set.
View Source
def acquire(self) -> contextlib.AbstractAsyncContextManager[typing.Any]: """Acquire this lazy constant as an asynchronous lock. This is used to ensure that the value is only generated once and should be kept acquired until `LazyConstant.set_value` has been called. Returns ------- contextlib.AbstractAsyncContextManager[typing.Any] Context manager that can be used to acquire the lock. """ if not self._lock: # Error if this is called outside of a running event loop. asyncio.get_running_loop() self._lock = asyncio.Lock() return self._lock
Acquire this lazy constant as an asynchronous lock.
This is used to ensure that the value is only generated once
and should be kept acquired until LazyConstant.set_value has
been called.
Returns
- contextlib.AbstractAsyncContextManager[typing.Any]: Context manager that can be used to acquire the lock.
View Source
def with_concurrency_limit( bucket_id: str, /, *, error_message: str = "This resource is currently busy; please try again later.", ) -> collections.Callable[[CommandT], CommandT]: """Add the hooks used to manage a command's concurrency limit through a decorator call. .. warning:: Concurrency limiters will only work if there's a setup injected `AbstractConcurrencyLimiter` dependency with `InMemoryConcurrencyLimiter` being usable as a standard in-memory concurrency manager. Parameters ---------- bucket_id : str The concurrency limit bucket's ID. Other Parameters ---------------- error_message : str The error message to send in response as a command error if this fails to acquire the concurrency limit. Defaults to "This resource is currently busy; please try again later.". Returns ------- collections.abc.Callable[[CommandT], CommandT] A decorator that adds the concurrency limiter hooks to a command. """ def decorator(command: CommandT, /) -> CommandT: hooks_ = command.hooks if not hooks_: hooks_ = hooks.AnyHooks() command.set_hooks(hooks_) hooks_.add_pre_execution(ConcurrencyPreExecution(bucket_id, error_message=error_message)).add_post_execution( ConcurrencyPostExecution(bucket_id) ) return command return decorator
Add the hooks used to manage a command's concurrency limit through a decorator call.
Warning:
Concurrency limiters will only work if there's a setup injected
AbstractConcurrencyLimiter dependency with InMemoryConcurrencyLimiter
being usable as a standard in-memory concurrency manager.
Parameters
- bucket_id (str): The concurrency limit bucket's ID.
Other Parameters
error_message (str): The error message to send in response as a command error if this fails to acquire the concurrency limit.
Defaults to "This resource is currently busy; please try again later.".
Returns
- collections.abc.Callable[[CommandT], CommandT]: A decorator that adds the concurrency limiter hooks to a command.
View Source
def with_cooldown( bucket_id: str, /, *, error_message: str = "Please wait {cooldown:0.2f} seconds before using this command again.", owners_exempt: bool = True, ) -> collections.Callable[[CommandT], CommandT]: """Add a pre-execution hook used to manage a command's cooldown through a decorator call. .. warning:: Cooldowns will only work if there's a setup injected `AbstractCooldownManager` dependency with `InMemoryCooldownManager` being usable as a standard in-memory cooldown manager. Parameters ---------- bucket_id : str The cooldown bucket's ID. Other Parameters ---------------- error_message : str The error message to send in response as a command error if the check fails. Defaults to f"Please wait {cooldown:0.2f} seconds before using this command again.". owners_exempt : bool Whether owners should be exempt from the cooldown. Defaults to `True`. Returns ------- collections.abc.Callable[[CommandT], CommandT] A decorator that adds a `CooldownPreExecution` hook to the command. """ def decorator(command: CommandT, /) -> CommandT: hooks_ = command.hooks if not hooks_: hooks_ = hooks.AnyHooks() command.set_hooks(hooks_) hooks_.add_pre_execution( CooldownPreExecution(bucket_id, error_message=error_message, owners_exempt=owners_exempt) ) return command return decorator
Add a pre-execution hook used to manage a command's cooldown through a decorator call.
Warning:
Cooldowns will only work if there's a setup injected AbstractCooldownManager
dependency with InMemoryCooldownManager being usable as a standard in-memory
cooldown manager.
Parameters
- bucket_id (str): The cooldown bucket's ID.
Other Parameters
error_message (str): The error message to send in response as a command error if the check fails.
Defaults to f"Please wait {cooldown:0.2f} seconds before using this command again.".
owners_exempt (bool): Whether owners should be exempt from the cooldown.
Defaults to
True.
Returns
- collections.abc.Callable[[CommandT], CommandT]: A decorator that adds a
CooldownPreExecutionhook to the command.
View Source
# -*- coding: utf-8 -*- # cython: language_level=3 # BSD 3-Clause License # # Copyright (c) 2020-2022, Faster Speeding # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """The errors and warnings raised within and by Tanjun.""" from __future__ import annotations __all__: list[str] = [ "CommandError", "ConversionError", "FailedCheck", "FailedModuleLoad", "FailedModuleUnload", "HaltExecution", "MissingDependencyError", "ModuleMissingLoaders", "ModuleStateConflict", "NotEnoughArgumentsError", "TooManyArgumentsError", "ParserError", "TanjunError", ] import typing if typing.TYPE_CHECKING: import pathlib from collections import abc as collections class TanjunError(Exception): """The base class for all errors raised by Tanjun.""" __slots__ = () class HaltExecution(TanjunError): """Error raised while looking for a command in-order to end-execution early. For the most part, this will be raised during checks in-order to prevent other commands from being tried. """ __slots__ = () class MissingDependencyError(TanjunError): """Error raised when a dependency couldn't be found.""" __slots__ = ("message",) message: str """The error's message.""" def __init__(self, message: str) -> None: """Initialise a missing dependency error. Parameters ---------- message : str The error message. """ self.message = message class CommandError(TanjunError): """Error raised to end command execution.""" __slots__ = ("message",) # None or empty string == no response message: str """The response error message. Tanjun will try to send the string message as a response. """ def __init__(self, message: str, /) -> None: """Initialise a command error. Parameters ---------- message : str String message which will be sent as a response to the message that triggered the current command. Raises ------ ValueError Raised when the message is over 2000 characters long or empty. """ if len(message) > 2000: raise ValueError("Error message cannot be over 2_000 characters long.") elif not message: raise ValueError("Response message must have at least 1 character.") self.message = message def __str__(self) -> str: return self.message or "" # TODO: use this class InvalidCheck(TanjunError, RuntimeError): # TODO: or/and warning? # TODO: InvalidCheckError """Error raised as an assertion that a check will never pass in the current environment.""" __slots__ = () class FailedCheck(TanjunError, RuntimeError): # TODO: FailedCheckError """Error raised as an alternative to returning `False` in a check.""" __slots__ = () class ParserError(TanjunError, ValueError): """Base error raised by a parser or parameter during parsing. .. note:: Expected errors raised by the parser will subclass this error. """ __slots__ = ("message", "parameter") message: str """String message for this error. .. note:: This may be used as a command response message. """ parameter: typing.Optional[str] """Name of the this was raised for. .. note:: This will be `builtin.None` if it was raised while parsing the provided message content. """ def __init__(self, message: str, parameter: typing.Optional[str], /) -> None: """Initialise a parser error. Parameters ---------- message : str String message for this error. parameter : typing.Optional[str] Name of the parameter which caused this error, should be `None` if not applicable. """ self.message = message self.parameter = parameter def __str__(self) -> str: return self.message class ConversionError(ParserError): """Error raised by a parser parameter when it failed to converter a value.""" __slots__ = ("errors",) errors: collections.Sequence[ValueError] """Sequence of the errors that were caught during conversion for this parameter.""" parameter: str """Name of the parameter this error was raised for.""" def __init__(self, message: str, parameter: str, /, errors: collections.Iterable[ValueError] = ()) -> None: """Initialise a conversion error. Parameters ---------- parameter : tanjun.abc.Parameter The parameter this was raised by. errors : collections.abc.Iterable[ValueError] An iterable of the source value errors which were raised during conversion. """ super().__init__(message, parameter) self.errors = tuple(errors) class NotEnoughArgumentsError(ParserError): """Error raised by the parser when not enough arguments are found for a parameter.""" __slots__ = () parameter: str """Name of the parameter this error was raised for.""" def __init__(self, message: str, parameter: str, /) -> None: """Initialise a not enough arguments error. Parameters ---------- message : str The error message. parameter : tanjun.abc.Parameter The parameter this error was raised for. """ super().__init__(message, parameter) class TooManyArgumentsError(ParserError): """Error raised by the parser when too many arguments are found for a parameter.""" __slots__ = () parameter: str """Name of the parameter this error was raised for.""" def __init__(self, message: str, parameter: str, /) -> None: """Initialise a too many arguments error. Parameters ---------- message : str The error message. parameter : tanjun.abc.Parameter The parameter this error was raised for. """ super().__init__(message, parameter) class ModuleMissingLoaders(RuntimeError, TanjunError): """Error raised when a module is missing loaders or unloaders.""" __slots__ = ("_message", "_path") def __init__(self, message: str, path: typing.Union[str, pathlib.Path], /) -> None: self._message = message self._path = path @property def message(self) -> str: """The error message.""" return self._message @property def path(self) -> typing.Union[str, pathlib.Path]: """The path of the module which is missing loaders or unloaders.""" return self._path class ModuleStateConflict(ValueError, TanjunError): """Error raised when a module cannot be (un)loaded due to a state conflict.""" __slots__ = ("_message", "_path") def __init__(self, message: str, path: typing.Union[str, pathlib.Path], /) -> None: self._message = message self._path = path @property def message(self) -> str: """The error message.""" return self._message @property def path(self) -> typing.Union[str, pathlib.Path]: """The path of the module which caused the error.""" return self._path class FailedModuleLoad(TanjunError): """Error raised when a module fails to load. This may be raised by the module failing to import or by one of its loaders erroring. This source error can be accessed at `FailedLoad.__cause__`. """ __slots__ = () __cause__: Exception """The root error.""" class FailedModuleUnload(TanjunError): """Error raised when a module fails to unload. This may be raised by the module failing to import or by one of its unloaders erroring. The source error can be accessed at `FailedUnload.__cause__`. """ __slots__ = () __cause__: Exception """The root error."""
The errors and warnings raised within and by Tanjun.
View Source
class CommandError(TanjunError): """Error raised to end command execution.""" __slots__ = ("message",) # None or empty string == no response message: str """The response error message. Tanjun will try to send the string message as a response. """ def __init__(self, message: str, /) -> None: """Initialise a command error. Parameters ---------- message : str String message which will be sent as a response to the message that triggered the current command. Raises ------ ValueError Raised when the message is over 2000 characters long or empty. """ if len(message) > 2000: raise ValueError("Error message cannot be over 2_000 characters long.") elif not message: raise ValueError("Response message must have at least 1 character.") self.message = message def __str__(self) -> str: return self.message or ""
Error raised to end command execution.
View Source
def __init__(self, message: str, /) -> None: """Initialise a command error. Parameters ---------- message : str String message which will be sent as a response to the message that triggered the current command. Raises ------ ValueError Raised when the message is over 2000 characters long or empty. """ if len(message) > 2000: raise ValueError("Error message cannot be over 2_000 characters long.") elif not message: raise ValueError("Response message must have at least 1 character.") self.message = message
Initialise a command error.
Parameters
- message (str): String message which will be sent as a response to the message that triggered the current command.
Raises
- ValueError: Raised when the message is over 2000 characters long or empty.
The response error message.
Tanjun will try to send the string message as a response.
Inherited Members
- builtins.BaseException
- with_traceback
- args
View Source
class ConversionError(ParserError): """Error raised by a parser parameter when it failed to converter a value.""" __slots__ = ("errors",) errors: collections.Sequence[ValueError] """Sequence of the errors that were caught during conversion for this parameter.""" parameter: str """Name of the parameter this error was raised for.""" def __init__(self, message: str, parameter: str, /, errors: collections.Iterable[ValueError] = ()) -> None: """Initialise a conversion error. Parameters ---------- parameter : tanjun.abc.Parameter The parameter this was raised by. errors : collections.abc.Iterable[ValueError] An iterable of the source value errors which were raised during conversion. """ super().__init__(message, parameter) self.errors = tuple(errors)
Error raised by a parser parameter when it failed to converter a value.
View Source
def __init__(self, message: str, parameter: str, /, errors: collections.Iterable[ValueError] = ()) -> None: """Initialise a conversion error. Parameters ---------- parameter : tanjun.abc.Parameter The parameter this was raised by. errors : collections.abc.Iterable[ValueError] An iterable of the source value errors which were raised during conversion. """ super().__init__(message, parameter) self.errors = tuple(errors)
Initialise a conversion error.
Parameters
- parameter (tanjun.abc.Parameter): The parameter this was raised by.
- errors (collections.abc.Iterable[ValueError]): An iterable of the source value errors which were raised during conversion.
Sequence of the errors that were caught during conversion for this parameter.
Name of the parameter this error was raised for.
Inherited Members
- builtins.BaseException
- with_traceback
- args
View Source
class FailedCheck(TanjunError, RuntimeError): # TODO: FailedCheckError """Error raised as an alternative to returning `False` in a check.""" __slots__ = ()
Error raised as an alternative to returning False in a check.
Inherited Members
- builtins.RuntimeError
- RuntimeError
- builtins.BaseException
- with_traceback
- args
View Source
class FailedModuleLoad(TanjunError): """Error raised when a module fails to load. This may be raised by the module failing to import or by one of its loaders erroring. This source error can be accessed at `FailedLoad.__cause__`. """ __slots__ = () __cause__: Exception """The root error."""
Error raised when a module fails to load.
This may be raised by the module failing to import or by one of its loaders erroring.
This source error can be accessed at FailedLoad.__cause__.
Inherited Members
- builtins.Exception
- Exception
- builtins.BaseException
- with_traceback
- args
View Source
class FailedModuleUnload(TanjunError): """Error raised when a module fails to unload. This may be raised by the module failing to import or by one of its unloaders erroring. The source error can be accessed at `FailedUnload.__cause__`. """ __slots__ = () __cause__: Exception """The root error."""
Error raised when a module fails to unload.
This may be raised by the module failing to import or by one of its unloaders erroring.
The source error can be accessed at FailedUnload.__cause__.
Inherited Members
- builtins.Exception
- Exception
- builtins.BaseException
- with_traceback
- args
View Source
class HaltExecution(TanjunError): """Error raised while looking for a command in-order to end-execution early. For the most part, this will be raised during checks in-order to prevent other commands from being tried. """ __slots__ = ()
Error raised while looking for a command in-order to end-execution early.
For the most part, this will be raised during checks in-order to prevent other commands from being tried.
Inherited Members
- builtins.Exception
- Exception
- builtins.BaseException
- with_traceback
- args
View Source
class MissingDependencyError(TanjunError): """Error raised when a dependency couldn't be found.""" __slots__ = ("message",) message: str """The error's message.""" def __init__(self, message: str) -> None: """Initialise a missing dependency error. Parameters ---------- message : str The error message. """ self.message = message
Error raised when a dependency couldn't be found.
View Source
def __init__(self, message: str) -> None: """Initialise a missing dependency error. Parameters ---------- message : str The error message. """ self.message = message
Initialise a missing dependency error.
Parameters
- message (str): The error message.
The error's message.
Inherited Members
- builtins.BaseException
- with_traceback
- args
View Source
class ModuleMissingLoaders(RuntimeError, TanjunError): """Error raised when a module is missing loaders or unloaders.""" __slots__ = ("_message", "_path") def __init__(self, message: str, path: typing.Union[str, pathlib.Path], /) -> None: self._message = message self._path = path @property def message(self) -> str: """The error message.""" return self._message @property def path(self) -> typing.Union[str, pathlib.Path]: """The path of the module which is missing loaders or unloaders.""" return self._path
Error raised when a module is missing loaders or unloaders.
View Source
def __init__(self, message: str, path: typing.Union[str, pathlib.Path], /) -> None: self._message = message self._path = path
The error message.
The path of the module which is missing loaders or unloaders.
Inherited Members
- builtins.BaseException
- with_traceback
- args
View Source
class ModuleStateConflict(ValueError, TanjunError): """Error raised when a module cannot be (un)loaded due to a state conflict.""" __slots__ = ("_message", "_path") def __init__(self, message: str, path: typing.Union[str, pathlib.Path], /) -> None: self._message = message self._path = path @property def message(self) -> str: """The error message.""" return self._message @property def path(self) -> typing.Union[str, pathlib.Path]: """The path of the module which caused the error.""" return self._path
Error raised when a module cannot be (un)loaded due to a state conflict.
View Source
def __init__(self, message: str, path: typing.Union[str, pathlib.Path], /) -> None: self._message = message self._path = path
The error message.
The path of the module which caused the error.
Inherited Members
- builtins.BaseException
- with_traceback
- args
View Source
class NotEnoughArgumentsError(ParserError): """Error raised by the parser when not enough arguments are found for a parameter.""" __slots__ = () parameter: str """Name of the parameter this error was raised for.""" def __init__(self, message: str, parameter: str, /) -> None: """Initialise a not enough arguments error. Parameters ---------- message : str The error message. parameter : tanjun.abc.Parameter The parameter this error was raised for. """ super().__init__(message, parameter)
Error raised by the parser when not enough arguments are found for a parameter.
View Source
def __init__(self, message: str, parameter: str, /) -> None: """Initialise a not enough arguments error. Parameters ---------- message : str The error message. parameter : tanjun.abc.Parameter The parameter this error was raised for. """ super().__init__(message, parameter)
Initialise a not enough arguments error.
Parameters
- message (str): The error message.
- parameter (tanjun.abc.Parameter): The parameter this error was raised for.
Name of the parameter this error was raised for.
Inherited Members
- builtins.BaseException
- with_traceback
- args
View Source
class TooManyArgumentsError(ParserError): """Error raised by the parser when too many arguments are found for a parameter.""" __slots__ = () parameter: str """Name of the parameter this error was raised for.""" def __init__(self, message: str, parameter: str, /) -> None: """Initialise a too many arguments error. Parameters ---------- message : str The error message. parameter : tanjun.abc.Parameter The parameter this error was raised for. """ super().__init__(message, parameter)
Error raised by the parser when too many arguments are found for a parameter.
View Source
def __init__(self, message: str, parameter: str, /) -> None: """Initialise a too many arguments error. Parameters ---------- message : str The error message. parameter : tanjun.abc.Parameter The parameter this error was raised for. """ super().__init__(message, parameter)
Initialise a too many arguments error.
Parameters
- message (str): The error message.
- parameter (tanjun.abc.Parameter): The parameter this error was raised for.
Name of the parameter this error was raised for.
Inherited Members
- builtins.BaseException
- with_traceback
- args
View Source
class ParserError(TanjunError, ValueError): """Base error raised by a parser or parameter during parsing. .. note:: Expected errors raised by the parser will subclass this error. """ __slots__ = ("message", "parameter") message: str """String message for this error. .. note:: This may be used as a command response message. """ parameter: typing.Optional[str] """Name of the this was raised for. .. note:: This will be `builtin.None` if it was raised while parsing the provided message content. """ def __init__(self, message: str, parameter: typing.Optional[str], /) -> None: """Initialise a parser error. Parameters ---------- message : str String message for this error. parameter : typing.Optional[str] Name of the parameter which caused this error, should be `None` if not applicable. """ self.message = message self.parameter = parameter def __str__(self) -> str: return self.message
Base error raised by a parser or parameter during parsing.
Note: Expected errors raised by the parser will subclass this error.
View Source
def __init__(self, message: str, parameter: typing.Optional[str], /) -> None: """Initialise a parser error. Parameters ---------- message : str String message for this error. parameter : typing.Optional[str] Name of the parameter which caused this error, should be `None` if not applicable. """ self.message = message self.parameter = parameter
Initialise a parser error.
Parameters
- message (str): String message for this error.
- parameter (typing.Optional[str]):
Name of the parameter which caused this error, should be
Noneif not applicable.
String message for this error.
Note: This may be used as a command response message.
Name of the this was raised for.
Note:
This will be builtin.None if it was raised while parsing the provided
message content.
Inherited Members
- builtins.BaseException
- with_traceback
- args
View Source
class TanjunError(Exception): """The base class for all errors raised by Tanjun.""" __slots__ = ()
The base class for all errors raised by Tanjun.
Inherited Members
- builtins.Exception
- Exception
- builtins.BaseException
- with_traceback
- args
View Source
# -*- coding: utf-8 -*- # cython: language_level=3 # BSD 3-Clause License # # Copyright (c) 2020-2022, Faster Speeding # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Standard implementation of Tanjun's command execution hook models.""" from __future__ import annotations __all__: list[str] = ["AnyHooks", "Hooks", "MessageHooks", "SlashHooks"] import asyncio import copy import typing from collections import abc as collections from . import abc from . import errors from . import injecting if typing.TypeVar: _HooksT = typing.TypeVar("_HooksT", bound="Hooks[typing.Any]") CommandT = typing.TypeVar("CommandT", bound=abc.ExecutableCommand[typing.Any]) class Hooks(abc.Hooks[abc.ContextT_contra]): """Standard implementation of `tanjun.abc.Hooks` used for command execution. `tanjun.abc.ContextT_contra` will either be `tanjun.abc.Context`, `tanjun.abc.MessageContext` or `tanjun.abc.SlashContext`. .. note:: This implementation adds a concept of parser errors which won't be dispatched to general "error" hooks and do not share the error suppression semantics as they favour to always suppress the error if a registered handler is found. """ __slots__ = ( "_error_callbacks", "_parser_error_callbacks", "_pre_execution_callbacks", "_post_execution_callbacks", "_success_callbacks", ) def __init__(self) -> None: """Initialise a command hook object.""" self._error_callbacks: list[injecting.CallbackDescriptor[typing.Union[bool, None]]] = [] self._parser_error_callbacks: list[injecting.CallbackDescriptor[None]] = [] self._pre_execution_callbacks: list[injecting.CallbackDescriptor[None]] = [] self._post_execution_callbacks: list[injecting.CallbackDescriptor[None]] = [] self._success_callbacks: list[injecting.CallbackDescriptor[None]] = [] def add_to_command(self, command: CommandT, /) -> CommandT: """Add this hook object to a command. .. note:: This will likely override any previously added hooks. Examples -------- This method may be used as a command decorator: ```py @standard_hooks.add_to_command @as_message_command("command") async def command_command(ctx: tanjun.abc.Context) -> None: await ctx.respond("You've called a command!") ``` Parameters ---------- command : tanjun.abc.ExecutableCommand[typing.Any] The command to add the hooks to. Returns ------- tanjun.abc.ExecutableCommand[typing.Any] The command with the hooks added. """ command.set_hooks(self) return command def copy(self: _HooksT) -> _HooksT: """Copy this hook object.""" return copy.deepcopy(self) # TODO: maybe don't def add_on_error(self: _HooksT, callback: abc.ErrorHookSig, /) -> _HooksT: # <<inherited docstring from tanjun.abc.Hooks>>. self._error_callbacks.append(injecting.CallbackDescriptor(callback)) return self def set_on_error(self: _HooksT, callback: typing.Optional[abc.ErrorHookSig], /) -> _HooksT: """Set the error callback for this hook object. .. note:: This will not be called for `tanjun.errors.ParserError`s as these are generally speaking expected. To handle those see `Hooks.set_on_parser_error`. Parameters ---------- callback : typing.Optional[tanjun.abc.ErrorHookSig] The callback to set for this hook. This will remove any previously set callbacks. This callback should take two positional arguments (of type `tanjun.abc.ContextT_contra` and `Exception`) and may be either synchronous or asynchronous. Returning `True` indicates that the error should be suppressed, `False` that it should be re-raised and `None` that no decision has been made. This will be accounted for along with the decisions other error hooks make by majority rule. Returns ------- Self The hook object to enable method chaining. """ self._error_callbacks.clear() return self.add_on_error(callback) if callback else self def with_on_error(self, callback: abc.ErrorHookSigT, /) -> abc.ErrorHookSigT: # <<inherited docstring from tanjun.abc.Hooks>>. self.add_on_error(callback) return callback def add_on_parser_error(self: _HooksT, callback: abc.HookSig, /) -> _HooksT: # <<inherited docstring from tanjun.abc.Hooks>>. self._parser_error_callbacks.append(injecting.CallbackDescriptor(callback)) return self def set_on_parser_error(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT: """Set the parser error callback for this hook object. Parameters ---------- callback : typing.Optional[tanjun.abc.HookSig] The callback to set for this hook. This will remove any previously set callbacks. This callback should take two positional arguments (of type `tanjun.abc.ContextT_contra` and `tanjun.errors.ParserError`), return `None` and may be either synchronous or asynchronous. It's worth noting that, unlike general error handlers, this will always suppress the error. Returns ------- Self The hook object to enable method chaining. """ self._parser_error_callbacks.clear() return self.add_on_parser_error(callback) if callback else self def with_on_parser_error(self, callback: abc.HookSigT, /) -> abc.HookSigT: # <<inherited docstring from tanjun.abc.Hooks>>. self.add_on_parser_error(callback) return callback def add_post_execution(self: _HooksT, callback: abc.HookSig, /) -> _HooksT: # <<inherited docstring from tanjun.abc.Hooks>>. self._post_execution_callbacks.append(injecting.CallbackDescriptor(callback)) return self def set_post_execution(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT: """Set the post-execution callback for this hook object. Parameters ---------- callback : typing.Optional[tanjun.abc.HookSig] The callback to set for this hook. This will remove any previously set callbacks. This callback should take one positional argument (of type `tanjun.abc.ContextT_contra`), return `None` and may be either synchronous or asynchronous. Returns ------- Self The hook object to enable method chaining. """ self._post_execution_callbacks.clear() return self.add_post_execution(callback) if callback else self def with_post_execution(self, callback: abc.HookSigT, /) -> abc.HookSigT: # <<inherited docstring from tanjun.abc.Hooks>>. self.add_post_execution(callback) return callback def add_pre_execution(self: _HooksT, callback: abc.HookSig, /) -> _HooksT: # <<inherited docstring from tanjun.abc.Hooks>>. self._pre_execution_callbacks.append(injecting.CallbackDescriptor(callback)) return self def set_pre_execution(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT: """Set the pre-execution callback for this hook object. Parameters ---------- callback : typing.Optional[tanjun.abc.HookSig] The callback to set for this hook. This will remove any previously set callbacks. This callback should take one positional argument (of type `tanjun.abc.ContextT_contra`), return `None` and may be either synchronous or asynchronous. Returns ------- Self The hook object to enable method chaining. """ self._pre_execution_callbacks.clear() return self.add_pre_execution(callback) if callback else self def with_pre_execution(self, callback: abc.HookSigT, /) -> abc.HookSigT: # <<inherited docstring from tanjun.abc.Hooks>>. self.add_pre_execution(callback) return callback def add_on_success(self: _HooksT, callback: abc.HookSig, /) -> _HooksT: # <<inherited docstring from tanjun.abc.Hooks>>. self._success_callbacks.append(injecting.CallbackDescriptor(callback)) return self def set_on_success(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT: """Set the success callback for this hook object. Parameters ---------- callback : typing.Optional[HookSig[tanjun.abc.HookSig]] The callback to set for this hook. This will remove any previously set callbacks. This callback should take one positional argument (of type `tanjun.abc.ContextT_contra`), return `None` and may be either synchronous or asynchronous. Returns ------- Self The hook object to enable method chaining. """ self._success_callbacks.clear() return self.add_on_success(callback) if callback else self def with_on_success(self, callback: abc.HookSigT, /) -> abc.HookSigT: # <<inherited docstring from tanjun.abc.Hooks>>. self.add_on_success(callback) return callback async def trigger_error( self, ctx: abc.ContextT_contra, /, exception: Exception, *, hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None, ) -> int: # <<inherited docstring from tanjun.abc.Hooks>>. level = 0 if isinstance(exception, errors.ParserError): if self._parser_error_callbacks: await asyncio.gather( *(c.resolve_with_command_context(ctx, ctx, exception) for c in self._parser_error_callbacks) ) level = 100 # We don't want to re-raise a parser error if it was caught elif self._error_callbacks: results = await asyncio.gather( *(c.resolve_with_command_context(ctx, ctx, exception) for c in self._error_callbacks) ) level = results.count(True) - results.count(False) if hooks: level += sum(await asyncio.gather(*(hook.trigger_error(ctx, exception) for hook in hooks))) return level async def trigger_post_execution( self, ctx: abc.ContextT_contra, /, *, hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None, ) -> None: # <<inherited docstring from tanjun.abc.Hooks>>. if self._post_execution_callbacks: await asyncio.gather(*(c.resolve_with_command_context(ctx, ctx) for c in self._post_execution_callbacks)) if hooks: await asyncio.gather(*(hook.trigger_post_execution(ctx) for hook in hooks)) async def trigger_pre_execution( self, ctx: abc.ContextT_contra, /, *, hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None, ) -> None: # <<inherited docstring from tanjun.abc.Hooks>>. if self._pre_execution_callbacks: await asyncio.gather(*(c.resolve_with_command_context(ctx, ctx) for c in self._pre_execution_callbacks)) if hooks: await asyncio.gather(*(hook.trigger_pre_execution(ctx) for hook in hooks)) async def trigger_success( self, ctx: abc.ContextT_contra, /, *, hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None, ) -> None: # <<inherited docstring from tanjun.abc.Hooks>>. if self._success_callbacks: await asyncio.gather(*(c.resolve_with_command_context(ctx, ctx) for c in self._success_callbacks)) if hooks: await asyncio.gather(*(hook.trigger_success(ctx) for hook in hooks)) AnyHooks = Hooks[abc.Context] """Hooks that can be used with any context. .. note:: This is shorthand for Hooks[tanjun.abc.Context]. """ MessageHooks = Hooks[abc.MessageContext] """Hooks that can be used with a message context. .. note:: This is shorthand for Hooks[tanjun.abc.MessageContext]. """ SlashHooks = Hooks[abc.SlashContext] """Hooks that can be used with a slash context. .. note:: This is shorthand for Hooks[tanjun.abc.SlashContext]. """
Standard implementation of Tanjun's command execution hook models.
View Source
class Hooks(abc.Hooks[abc.ContextT_contra]): """Standard implementation of `tanjun.abc.Hooks` used for command execution. `tanjun.abc.ContextT_contra` will either be `tanjun.abc.Context`, `tanjun.abc.MessageContext` or `tanjun.abc.SlashContext`. .. note:: This implementation adds a concept of parser errors which won't be dispatched to general "error" hooks and do not share the error suppression semantics as they favour to always suppress the error if a registered handler is found. """ __slots__ = ( "_error_callbacks", "_parser_error_callbacks", "_pre_execution_callbacks", "_post_execution_callbacks", "_success_callbacks", ) def __init__(self) -> None: """Initialise a command hook object.""" self._error_callbacks: list[injecting.CallbackDescriptor[typing.Union[bool, None]]] = [] self._parser_error_callbacks: list[injecting.CallbackDescriptor[None]] = [] self._pre_execution_callbacks: list[injecting.CallbackDescriptor[None]] = [] self._post_execution_callbacks: list[injecting.CallbackDescriptor[None]] = [] self._success_callbacks: list[injecting.CallbackDescriptor[None]] = [] def add_to_command(self, command: CommandT, /) -> CommandT: """Add this hook object to a command. .. note:: This will likely override any previously added hooks. Examples -------- This method may be used as a command decorator: ```py @standard_hooks.add_to_command @as_message_command("command") async def command_command(ctx: tanjun.abc.Context) -> None: await ctx.respond("You've called a command!") ``` Parameters ---------- command : tanjun.abc.ExecutableCommand[typing.Any] The command to add the hooks to. Returns ------- tanjun.abc.ExecutableCommand[typing.Any] The command with the hooks added. """ command.set_hooks(self) return command def copy(self: _HooksT) -> _HooksT: """Copy this hook object.""" return copy.deepcopy(self) # TODO: maybe don't def add_on_error(self: _HooksT, callback: abc.ErrorHookSig, /) -> _HooksT: # <<inherited docstring from tanjun.abc.Hooks>>. self._error_callbacks.append(injecting.CallbackDescriptor(callback)) return self def set_on_error(self: _HooksT, callback: typing.Optional[abc.ErrorHookSig], /) -> _HooksT: """Set the error callback for this hook object. .. note:: This will not be called for `tanjun.errors.ParserError`s as these are generally speaking expected. To handle those see `Hooks.set_on_parser_error`. Parameters ---------- callback : typing.Optional[tanjun.abc.ErrorHookSig] The callback to set for this hook. This will remove any previously set callbacks. This callback should take two positional arguments (of type `tanjun.abc.ContextT_contra` and `Exception`) and may be either synchronous or asynchronous. Returning `True` indicates that the error should be suppressed, `False` that it should be re-raised and `None` that no decision has been made. This will be accounted for along with the decisions other error hooks make by majority rule. Returns ------- Self The hook object to enable method chaining. """ self._error_callbacks.clear() return self.add_on_error(callback) if callback else self def with_on_error(self, callback: abc.ErrorHookSigT, /) -> abc.ErrorHookSigT: # <<inherited docstring from tanjun.abc.Hooks>>. self.add_on_error(callback) return callback def add_on_parser_error(self: _HooksT, callback: abc.HookSig, /) -> _HooksT: # <<inherited docstring from tanjun.abc.Hooks>>. self._parser_error_callbacks.append(injecting.CallbackDescriptor(callback)) return self def set_on_parser_error(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT: """Set the parser error callback for this hook object. Parameters ---------- callback : typing.Optional[tanjun.abc.HookSig] The callback to set for this hook. This will remove any previously set callbacks. This callback should take two positional arguments (of type `tanjun.abc.ContextT_contra` and `tanjun.errors.ParserError`), return `None` and may be either synchronous or asynchronous. It's worth noting that, unlike general error handlers, this will always suppress the error. Returns ------- Self The hook object to enable method chaining. """ self._parser_error_callbacks.clear() return self.add_on_parser_error(callback) if callback else self def with_on_parser_error(self, callback: abc.HookSigT, /) -> abc.HookSigT: # <<inherited docstring from tanjun.abc.Hooks>>. self.add_on_parser_error(callback) return callback def add_post_execution(self: _HooksT, callback: abc.HookSig, /) -> _HooksT: # <<inherited docstring from tanjun.abc.Hooks>>. self._post_execution_callbacks.append(injecting.CallbackDescriptor(callback)) return self def set_post_execution(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT: """Set the post-execution callback for this hook object. Parameters ---------- callback : typing.Optional[tanjun.abc.HookSig] The callback to set for this hook. This will remove any previously set callbacks. This callback should take one positional argument (of type `tanjun.abc.ContextT_contra`), return `None` and may be either synchronous or asynchronous. Returns ------- Self The hook object to enable method chaining. """ self._post_execution_callbacks.clear() return self.add_post_execution(callback) if callback else self def with_post_execution(self, callback: abc.HookSigT, /) -> abc.HookSigT: # <<inherited docstring from tanjun.abc.Hooks>>. self.add_post_execution(callback) return callback def add_pre_execution(self: _HooksT, callback: abc.HookSig, /) -> _HooksT: # <<inherited docstring from tanjun.abc.Hooks>>. self._pre_execution_callbacks.append(injecting.CallbackDescriptor(callback)) return self def set_pre_execution(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT: """Set the pre-execution callback for this hook object. Parameters ---------- callback : typing.Optional[tanjun.abc.HookSig] The callback to set for this hook. This will remove any previously set callbacks. This callback should take one positional argument (of type `tanjun.abc.ContextT_contra`), return `None` and may be either synchronous or asynchronous. Returns ------- Self The hook object to enable method chaining. """ self._pre_execution_callbacks.clear() return self.add_pre_execution(callback) if callback else self def with_pre_execution(self, callback: abc.HookSigT, /) -> abc.HookSigT: # <<inherited docstring from tanjun.abc.Hooks>>. self.add_pre_execution(callback) return callback def add_on_success(self: _HooksT, callback: abc.HookSig, /) -> _HooksT: # <<inherited docstring from tanjun.abc.Hooks>>. self._success_callbacks.append(injecting.CallbackDescriptor(callback)) return self def set_on_success(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT: """Set the success callback for this hook object. Parameters ---------- callback : typing.Optional[HookSig[tanjun.abc.HookSig]] The callback to set for this hook. This will remove any previously set callbacks. This callback should take one positional argument (of type `tanjun.abc.ContextT_contra`), return `None` and may be either synchronous or asynchronous. Returns ------- Self The hook object to enable method chaining. """ self._success_callbacks.clear() return self.add_on_success(callback) if callback else self def with_on_success(self, callback: abc.HookSigT, /) -> abc.HookSigT: # <<inherited docstring from tanjun.abc.Hooks>>. self.add_on_success(callback) return callback async def trigger_error( self, ctx: abc.ContextT_contra, /, exception: Exception, *, hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None, ) -> int: # <<inherited docstring from tanjun.abc.Hooks>>. level = 0 if isinstance(exception, errors.ParserError): if self._parser_error_callbacks: await asyncio.gather( *(c.resolve_with_command_context(ctx, ctx, exception) for c in self._parser_error_callbacks) ) level = 100 # We don't want to re-raise a parser error if it was caught elif self._error_callbacks: results = await asyncio.gather( *(c.resolve_with_command_context(ctx, ctx, exception) for c in self._error_callbacks) ) level = results.count(True) - results.count(False) if hooks: level += sum(await asyncio.gather(*(hook.trigger_error(ctx, exception) for hook in hooks))) return level async def trigger_post_execution( self, ctx: abc.ContextT_contra, /, *, hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None, ) -> None: # <<inherited docstring from tanjun.abc.Hooks>>. if self._post_execution_callbacks: await asyncio.gather(*(c.resolve_with_command_context(ctx, ctx) for c in self._post_execution_callbacks)) if hooks: await asyncio.gather(*(hook.trigger_post_execution(ctx) for hook in hooks)) async def trigger_pre_execution( self, ctx: abc.ContextT_contra, /, *, hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None, ) -> None: # <<inherited docstring from tanjun.abc.Hooks>>. if self._pre_execution_callbacks: await asyncio.gather(*(c.resolve_with_command_context(ctx, ctx) for c in self._pre_execution_callbacks)) if hooks: await asyncio.gather(*(hook.trigger_pre_execution(ctx) for hook in hooks)) async def trigger_success( self, ctx: abc.ContextT_contra, /, *, hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None, ) -> None: # <<inherited docstring from tanjun.abc.Hooks>>. if self._success_callbacks: await asyncio.gather(*(c.resolve_with_command_context(ctx, ctx) for c in self._success_callbacks)) if hooks: await asyncio.gather(*(hook.trigger_success(ctx) for hook in hooks))
Standard implementation of tanjun.abc.Hooks used for command execution.
tanjun.abc.ContextT_contra will either be tanjun.abc.Context,
tanjun.abc.MessageContext or tanjun.abc.SlashContext.
Note: This implementation adds a concept of parser errors which won't be dispatched to general "error" hooks and do not share the error suppression semantics as they favour to always suppress the error if a registered handler is found.
View Source
def __init__(self) -> None: """Initialise a command hook object.""" self._error_callbacks: list[injecting.CallbackDescriptor[typing.Union[bool, None]]] = [] self._parser_error_callbacks: list[injecting.CallbackDescriptor[None]] = [] self._pre_execution_callbacks: list[injecting.CallbackDescriptor[None]] = [] self._post_execution_callbacks: list[injecting.CallbackDescriptor[None]] = [] self._success_callbacks: list[injecting.CallbackDescriptor[None]] = []
Initialise a command hook object.
View Source
def add_to_command(self, command: CommandT, /) -> CommandT: """Add this hook object to a command. .. note:: This will likely override any previously added hooks. Examples -------- This method may be used as a command decorator: ```py @standard_hooks.add_to_command @as_message_command("command") async def command_command(ctx: tanjun.abc.Context) -> None: await ctx.respond("You've called a command!") ``` Parameters ---------- command : tanjun.abc.ExecutableCommand[typing.Any] The command to add the hooks to. Returns ------- tanjun.abc.ExecutableCommand[typing.Any] The command with the hooks added. """ command.set_hooks(self) return command
Add this hook object to a command.
Note: This will likely override any previously added hooks.
Examples
This method may be used as a command decorator:
@standard_hooks.add_to_command
@as_message_command("command")
async def command_command(ctx: tanjun.abc.Context) -> None:
await ctx.respond("You've called a command!")
Parameters
- command (tanjun.abc.ExecutableCommand[typing.Any]): The command to add the hooks to.
Returns
- tanjun.abc.ExecutableCommand[typing.Any]: The command with the hooks added.
View Source
def copy(self: _HooksT) -> _HooksT: """Copy this hook object.""" return copy.deepcopy(self) # TODO: maybe don't
Copy this hook object.
View Source
def add_on_error(self: _HooksT, callback: abc.ErrorHookSig, /) -> _HooksT: # <<inherited docstring from tanjun.abc.Hooks>>. self._error_callbacks.append(injecting.CallbackDescriptor(callback)) return self
Add an error callback to this hook object.
Note:
This won't be called for expected tanjun.TanjunError derived errors.
Parameters
callback (ErrorHookSig): The callback to add to this hook.
This callback should take two positional arguments (of type
tanjun.abc.ContextT_contraandException) and may be either synchronous or asynchronous.Returning
Trueindicates that the error should be suppressed,Falsethat it should be re-raised andNonethat no decision has been made. This will be accounted for along with the decisions other error hooks make by majority rule.
Returns
- Self: The hook object to enable method chaining.
View Source
def set_on_error(self: _HooksT, callback: typing.Optional[abc.ErrorHookSig], /) -> _HooksT: """Set the error callback for this hook object. .. note:: This will not be called for `tanjun.errors.ParserError`s as these are generally speaking expected. To handle those see `Hooks.set_on_parser_error`. Parameters ---------- callback : typing.Optional[tanjun.abc.ErrorHookSig] The callback to set for this hook. This will remove any previously set callbacks. This callback should take two positional arguments (of type `tanjun.abc.ContextT_contra` and `Exception`) and may be either synchronous or asynchronous. Returning `True` indicates that the error should be suppressed, `False` that it should be re-raised and `None` that no decision has been made. This will be accounted for along with the decisions other error hooks make by majority rule. Returns ------- Self The hook object to enable method chaining. """ self._error_callbacks.clear() return self.add_on_error(callback) if callback else self
Set the error callback for this hook object.
Note:
This will not be called for tanjun.errors.ParserErrors as these
are generally speaking expected. To handle those see
Hooks.set_on_parser_error.
Parameters
callback (typing.Optional[tanjun.abc.ErrorHookSig]): The callback to set for this hook. This will remove any previously set callbacks.
This callback should take two positional arguments (of type
tanjun.abc.ContextT_contraandException) and may be either synchronous or asynchronous.Returning
Trueindicates that the error should be suppressed,Falsethat it should be re-raised andNonethat no decision has been made. This will be accounted for along with the decisions other error hooks make by majority rule.
Returns
- Self: The hook object to enable method chaining.
View Source
def with_on_error(self, callback: abc.ErrorHookSigT, /) -> abc.ErrorHookSigT: # <<inherited docstring from tanjun.abc.Hooks>>. self.add_on_error(callback) return callback
Add an error callback to this hook object through a decorator call.
Note:
This won't be called for expected tanjun.TanjunError derived errors.
Examples
hooks = AnyHooks()
@hooks.with_on_error
async def on_error(ctx: tanjun.abc.Context, error: Exception) -> bool:
if isinstance(error, SomeExpectedType):
await ctx.respond("You dun goofed")
return True # Indicating that it should be suppressed.
await ctx.respond(f"An error occurred: {error}")
return False # Indicating that it should be re-raised
Parameters
callback (ErrorHookSigT): The callback to add to this hook.
This callback should take two positional arguments (of type
tanjun.abc.ContextT_contraandException) and may be either synchronous or asynchronous.Returning
Trueindicates that the error shoul be suppressed,Falsethat it should be re-raised andNonethat no decision has been made. This will be accounted for along with the decisions other error hooks make by majority rule.
Returns
- ErrorHookSigT: The hook callback which was added.
View Source
def add_on_parser_error(self: _HooksT, callback: abc.HookSig, /) -> _HooksT: # <<inherited docstring from tanjun.abc.Hooks>>. self._parser_error_callbacks.append(injecting.CallbackDescriptor(callback)) return self
Add a parser error callback to this hook object.
Parameters
callback (HookSig): The callback to add to this hook.
This callback should take two positional arguments (of type
tanjun.abc.ContextT_contraandtanjun.errors.ParserError), returnNoneand may be either synchronous or asynchronous.It's worth noting that this unlike general error handlers, this will always suppress the error.
Returns
- Self: The hook object to enable method chaining.
View Source
def set_on_parser_error(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT: """Set the parser error callback for this hook object. Parameters ---------- callback : typing.Optional[tanjun.abc.HookSig] The callback to set for this hook. This will remove any previously set callbacks. This callback should take two positional arguments (of type `tanjun.abc.ContextT_contra` and `tanjun.errors.ParserError`), return `None` and may be either synchronous or asynchronous. It's worth noting that, unlike general error handlers, this will always suppress the error. Returns ------- Self The hook object to enable method chaining. """ self._parser_error_callbacks.clear() return self.add_on_parser_error(callback) if callback else self
Set the parser error callback for this hook object.
Parameters
callback (typing.Optional[tanjun.abc.HookSig]): The callback to set for this hook. This will remove any previously set callbacks.
This callback should take two positional arguments (of type
tanjun.abc.ContextT_contraandtanjun.errors.ParserError), returnNoneand may be either synchronous or asynchronous.It's worth noting that, unlike general error handlers, this will always suppress the error.
Returns
- Self: The hook object to enable method chaining.
View Source
def with_on_parser_error(self, callback: abc.HookSigT, /) -> abc.HookSigT: # <<inherited docstring from tanjun.abc.Hooks>>. self.add_on_parser_error(callback) return callback
Add a parser error callback to this hook object through a decorator call.
Examples
hooks = AnyHooks()
@hooks.with_on_parser_error
async def on_parser_error(ctx: tanjun.abc.Context, error: tanjun.errors.ParserError) -> None:
await ctx.respond(f"You gave invalid input: {error}")
Parameters
callback (HookSigT): The parser error callback to add to this hook.
This callback should take two positional arguments (of type
tanjun.abc.ContextT_contraandtanjun.errors.ParserError), returnNoneand may be either synchronous or asynchronous.
Returns
- HookSigT: The callback which was added.
View Source
def add_post_execution(self: _HooksT, callback: abc.HookSig, /) -> _HooksT: # <<inherited docstring from tanjun.abc.Hooks>>. self._post_execution_callbacks.append(injecting.CallbackDescriptor(callback)) return self
Add a post-execution callback to this hook object.
Parameters
callback (HookSig): The callback to add to this hook.
This callback should take one positional argument (of type
tanjun.abc.ContextT_contra), returnNoneand may be either synchronous or asynchronous.
Returns
- Self: The hook object to enable method chaining.
View Source
def set_post_execution(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT: """Set the post-execution callback for this hook object. Parameters ---------- callback : typing.Optional[tanjun.abc.HookSig] The callback to set for this hook. This will remove any previously set callbacks. This callback should take one positional argument (of type `tanjun.abc.ContextT_contra`), return `None` and may be either synchronous or asynchronous. Returns ------- Self The hook object to enable method chaining. """ self._post_execution_callbacks.clear() return self.add_post_execution(callback) if callback else self
Set the post-execution callback for this hook object.
Parameters
callback (typing.Optional[tanjun.abc.HookSig]): The callback to set for this hook. This will remove any previously set callbacks.
This callback should take one positional argument (of type
tanjun.abc.ContextT_contra), returnNoneand may be either synchronous or asynchronous.
Returns
- Self: The hook object to enable method chaining.
View Source
def with_post_execution(self, callback: abc.HookSigT, /) -> abc.HookSigT: # <<inherited docstring from tanjun.abc.Hooks>>. self.add_post_execution(callback) return callback
Add a post-execution callback to this hook object through a decorator call.
Examples
hooks = AnyHooks()
@hooks.with_post_execution
async def post_execution(ctx: tanjun.abc.Context) -> None:
await ctx.respond("You did something")
Parameters
callback (HookSigT): The post-execution callback to add to this hook.
This callback should take one positional argument (of type
tanjun.abc.ContextT_contra), returnNoneand may be either synchronous or asynchronous.
Returns
- HookSigT: The post-execution callback which was seaddedt.
View Source
def add_pre_execution(self: _HooksT, callback: abc.HookSig, /) -> _HooksT: # <<inherited docstring from tanjun.abc.Hooks>>. self._pre_execution_callbacks.append(injecting.CallbackDescriptor(callback)) return self
Add a pre-execution callback for this hook object.
Parameters
callback (HookSig): The callback to add to this hook.
This callback should take one positional argument (of type
tanjun.abc.ContextT_contra), returnNoneand may be either synchronous or asynchronous.
Returns
- Self: The hook object to enable method chaining.
View Source
def set_pre_execution(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT: """Set the pre-execution callback for this hook object. Parameters ---------- callback : typing.Optional[tanjun.abc.HookSig] The callback to set for this hook. This will remove any previously set callbacks. This callback should take one positional argument (of type `tanjun.abc.ContextT_contra`), return `None` and may be either synchronous or asynchronous. Returns ------- Self The hook object to enable method chaining. """ self._pre_execution_callbacks.clear() return self.add_pre_execution(callback) if callback else self
Set the pre-execution callback for this hook object.
Parameters
callback (typing.Optional[tanjun.abc.HookSig]): The callback to set for this hook. This will remove any previously set callbacks.
This callback should take one positional argument (of type
tanjun.abc.ContextT_contra), returnNoneand may be either synchronous or asynchronous.
Returns
- Self: The hook object to enable method chaining.
View Source
def with_pre_execution(self, callback: abc.HookSigT, /) -> abc.HookSigT: # <<inherited docstring from tanjun.abc.Hooks>>. self.add_pre_execution(callback) return callback
Add a pre-execution callback to this hook object through a decorator call.
Examples
hooks = AnyHooks()
@hooks.with_pre_execution
async def pre_execution(ctx: tanjun.abc.Context) -> None:
await ctx.respond("You did something")
Parameters
callback (HookSigT): The pre-execution callback to add to this hook.
This callback should take one positional argument (of type
tanjun.abc.ContextT_contra), returnNoneand may be either synchronous or asynchronous.
Returns
- HookSigT: The pre-execution callback which was added.
View Source
def add_on_success(self: _HooksT, callback: abc.HookSig, /) -> _HooksT: # <<inherited docstring from tanjun.abc.Hooks>>. self._success_callbacks.append(injecting.CallbackDescriptor(callback)) return self
Add a success callback to this hook object.
Parameters
callback (HookSig): The callback to add to this hook.
This callback should take one positional argument (of type
tanjun.abc.ContextT_contra), returnNoneand may be either synchronous or asynchronous.
Returns
- Self: The hook object to enable method chaining.
View Source
def set_on_success(self: _HooksT, callback: typing.Optional[abc.HookSig], /) -> _HooksT: """Set the success callback for this hook object. Parameters ---------- callback : typing.Optional[HookSig[tanjun.abc.HookSig]] The callback to set for this hook. This will remove any previously set callbacks. This callback should take one positional argument (of type `tanjun.abc.ContextT_contra`), return `None` and may be either synchronous or asynchronous. Returns ------- Self The hook object to enable method chaining. """ self._success_callbacks.clear() return self.add_on_success(callback) if callback else self
Set the success callback for this hook object.
Parameters
callback (typing.Optional[HookSig[tanjun.abc.HookSig]]): The callback to set for this hook. This will remove any previously set callbacks.
This callback should take one positional argument (of type
tanjun.abc.ContextT_contra), returnNoneand may be either synchronous or asynchronous.
Returns
- Self: The hook object to enable method chaining.
View Source
def with_on_success(self, callback: abc.HookSigT, /) -> abc.HookSigT: # <<inherited docstring from tanjun.abc.Hooks>>. self.add_on_success(callback) return callback
Add a success callback to this hook object through a decorator call.
Examples
hooks = AnyHooks()
@hooks.with_on_success
async def on_success(ctx: tanjun.abc.Context) -> None:
await ctx.respond("You did something")
Parameters
callback (HookSigT): The success callback to add to this hook.
This callback should take one positional argument (of type
tanjun.abc.ContextT_contra), returnNoneand may be either synchronous or asynchronous.
Returns
- HookSigT: The success callback which was added.
View Source
async def trigger_error( self, ctx: abc.ContextT_contra, /, exception: Exception, *, hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None, ) -> int: # <<inherited docstring from tanjun.abc.Hooks>>. level = 0 if isinstance(exception, errors.ParserError): if self._parser_error_callbacks: await asyncio.gather( *(c.resolve_with_command_context(ctx, ctx, exception) for c in self._parser_error_callbacks) ) level = 100 # We don't want to re-raise a parser error if it was caught elif self._error_callbacks: results = await asyncio.gather( *(c.resolve_with_command_context(ctx, ctx, exception) for c in self._error_callbacks) ) level = results.count(True) - results.count(False) if hooks: level += sum(await asyncio.gather(*(hook.trigger_error(ctx, exception) for hook in hooks))) return level
View Source
async def trigger_post_execution( self, ctx: abc.ContextT_contra, /, *, hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None, ) -> None: # <<inherited docstring from tanjun.abc.Hooks>>. if self._post_execution_callbacks: await asyncio.gather(*(c.resolve_with_command_context(ctx, ctx) for c in self._post_execution_callbacks)) if hooks: await asyncio.gather(*(hook.trigger_post_execution(ctx) for hook in hooks))
View Source
async def trigger_pre_execution( self, ctx: abc.ContextT_contra, /, *, hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None, ) -> None: # <<inherited docstring from tanjun.abc.Hooks>>. if self._pre_execution_callbacks: await asyncio.gather(*(c.resolve_with_command_context(ctx, ctx) for c in self._pre_execution_callbacks)) if hooks: await asyncio.gather(*(hook.trigger_pre_execution(ctx) for hook in hooks))
View Source
async def trigger_success( self, ctx: abc.ContextT_contra, /, *, hooks: typing.Optional[collections.Set[abc.Hooks[abc.ContextT_contra]]] = None, ) -> None: # <<inherited docstring from tanjun.abc.Hooks>>. if self._success_callbacks: await asyncio.gather(*(c.resolve_with_command_context(ctx, ctx) for c in self._success_callbacks)) if hooks: await asyncio.gather(*(hook.trigger_success(ctx) for hook in hooks))
View Source
# -*- coding: utf-8 -*- # cython: language_level=3 # BSD 3-Clause License # # Copyright (c) 2020-2022, Faster Speeding # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Logic and data classes used within the standard Tanjun implementation to enable dependency injection.""" from __future__ import annotations __all__: list[str] = [ "AbstractDescriptor", "AbstractInjectionContext", "as_self_injecting", "BasicInjectionContext", "CallbackDescriptor", "CallbackSig", "Undefined", "UNDEFINED", "UndefinedOr", "inject", "injected", "Injected", "InjectorClient", "SelfInjectingCallback", "TypeDescriptor", ] import abc import collections.abc as collections import copy import inspect import sys import types import typing from . import abc as tanjun_abc from . import errors if typing.TYPE_CHECKING: _BasicInjectionContextT = typing.TypeVar("_BasicInjectionContextT", bound="BasicInjectionContext") _CallbackDescriptorT = typing.TypeVar("_CallbackDescriptorT", bound="CallbackDescriptor[typing.Any]") _InjectorClientT = typing.TypeVar("_InjectorClientT", bound="InjectorClient") _T = typing.TypeVar("_T") CallbackSig = collections.Callable[..., tanjun_abc.MaybeAwaitableT[_T]] """Type-hint of a injector callback. .. note:: Dependency dependency injection is recursively supported, meaning that the keyword arguments for a dependency callback may also ask for dependencies themselves. This may either be a synchronous or asynchronous function with dependency injection being available for the callback's keyword arguments but dynamically returning either an awaitable or raw value may lead to errors. Dependent on the context positional arguments may also be proivded. """ class Undefined: """Class/type of `UNDEFINED`.""" __instance: Undefined def __bool__(self) -> typing.Literal[False]: return False def __new__(cls) -> Undefined: try: return cls.__instance except AttributeError: new = super().__new__(cls) assert isinstance(new, Undefined) cls.__instance = new return cls.__instance UNDEFINED: typing.Final[Undefined] = Undefined() """Singleton value used within dependency injection to indicate that a value is undefined.""" UndefinedOr = typing.Union[Undefined, _T] """Type-hint generic union used to indicate that a value may be undefined or `_T`.""" class AbstractInjectionContext(abc.ABC): """Abstract interface of an injection context.""" __slots__ = () @property @abc.abstractmethod def injection_client(self) -> InjectorClient: """Injection client this context is bound to.""" @abc.abstractmethod def cache_result(self, callback: CallbackSig[_T], value: _T, /) -> None: """Cache the result of a callback within the scope of this context. Parameters ---------- callback : CallbackSig[_T] The callback to cache the result of. value : _T The value to cache. """ @abc.abstractmethod def get_cached_result(self, callback: CallbackSig[_T], /) -> UndefinedOr[_T]: """Get the cached result of a callback. Parameters ---------- callback : CallbackSig[_T] The callback to get the cached result of. Returns ------- UndefinedOr[_T] The cached result of the callback, or `UNDEFINED` if the callback has not been cached within this context. """ @abc.abstractmethod def get_type_dependency(self, type_: type[_T], /) -> UndefinedOr[_T]: """Get the implementation for an injected type. .. note:: Unlike `InjectionClient.get_type_dependency`, this method may also return context specific implementations of a type if the type isn't registered with the client. Parameters ---------- type_: type[_T] The associated type. Returns ------- UndefinedOr[_T] The resolved type if found, else `Undefined`. """ class BasicInjectionContext(AbstractInjectionContext): """Basic implementation of a `AbstractInjectionContext`.""" __slots__ = ("_injection_client", "_result_cache", "_special_case_types") def __init__(self, client: InjectorClient, /) -> None: """Initialise a basic injection context. Parameters ---------- client : InjectorClient The injection client this context is bound to. """ self._injection_client = client self._result_cache: typing.Optional[dict[CallbackSig[typing.Any], typing.Any]] = None self._special_case_types: dict[type[typing.Any], typing.Any] = { AbstractInjectionContext: self, BasicInjectionContext: self, type(self): self, } @property def injection_client(self) -> InjectorClient: # <<inherited docstring from AbstractInjectionContext>>. return self._injection_client def cache_result(self, callback: CallbackSig[_T], value: _T, /) -> None: # <<inherited docstring from AbstractInjectionContext>>. if self._result_cache is None: self._result_cache = {} self._result_cache[callback] = value def get_cached_result(self, callback: CallbackSig[_T], /) -> UndefinedOr[_T]: # <<inherited docstring from AbstractInjectionContext>>. return self._result_cache.get(callback, UNDEFINED) if self._result_cache else UNDEFINED def get_type_dependency(self, type_: type[_T], /) -> UndefinedOr[_T]: # <<inherited docstring from AbstractInjectionContext>>. if (value := self._special_case_types.get(type_, UNDEFINED)) is not UNDEFINED: return value return self._injection_client.get_type_dependency(type_) def _set_type_special_case(self: _BasicInjectionContextT, type_: type[_T], value: _T, /) -> _BasicInjectionContextT: self._special_case_types[type_] = value return self def _remove_type_special_case(self: _BasicInjectionContextT, type_: type[typing.Any], /) -> _BasicInjectionContextT: del self._special_case_types[type_] return self class AbstractDescriptor(abc.ABC, typing.Generic[_T]): """Abstract class for all injected argument descriptors.""" __slots__ = () @property @abc.abstractmethod def needs_injector(self) -> bool: """Whether this descriptor needs a dependency injection client to run.""" @abc.abstractmethod async def resolve_with_command_context(self, ctx: tanjun_abc.Context, /) -> _T: """Try to resolve the descriptor with the given command context. Parameters ---------- ctx : tanjun.abc.Context The context to resolve the descriptor with. Returns ------- _T The result to be injected. Raises ------ RuntimeError If the command context does not have a dependency injection client when one is required. tanjun.errors.MissingDependencyError If the client does not have an implementation of a non-defaulting type dependency this descriptor needs. """ @abc.abstractmethod async def resolve_without_injector(self) -> _T: """Try to resolve this descriptor without a dependency injection client. Returns ------- _T The result to be injected. Raises ------ RuntimeError If a dependency injection client is required. tanjun.errors.MissingDependencyError If the client does not have an implementation of a non-defaulting type dependency this descriptor needs. """ @abc.abstractmethod async def resolve(self, ctx: AbstractInjectionContext, /) -> _T: """Resolve the descriptor with the given dependency injection context. Parameters ---------- ctx : tanjun.abc.AbstractInjectionContext The context to resolve the type or callback with. Returns ------- _T The result to be injected. Raises ------ tanjun.errors.MissingDependencyError If the client does not have an implementation of a non-defaulting type dependency this descriptor needs. """ class CallbackDescriptor(AbstractDescriptor[_T]): """Descriptor of a callback taking advantage of dependency injection. This holds metadata and logic necessary for callback injection. """ __slots__ = ("_callback", "_descriptors", "_is_async", "_needs_injector") def __init__(self, callback: CallbackSig[_T], /) -> None: """Initialise an injected callback descriptor. Parameters ---------- callback : CallbackSig[_T] The callback to wrap with dependency injection. Raises ------ ValueError If `callback` has any injected arguments which can only be passed positionally. """ self._callback = callback self._is_async: typing.Optional[bool] = None self._descriptors, self._needs_injector = self._parse_descriptors(callback) # This is delegated to the callback to delegate set/list behaviour for this class to the callback. def __eq__(self, other: typing.Any) -> bool: return bool(self._callback == other) # This is delegated to the callback to delegate set/list behaviour for this class to the callback. def __hash__(self) -> int: return hash(self._callback) @property def callback(self) -> CallbackSig[_T]: """The descriptor's callback.""" return self._callback @property def needs_injector(self) -> bool: # <<inherited docstring from Descriptor>>. return self._needs_injector @staticmethod def _parse_descriptors(callback: CallbackSig[_T], /) -> tuple[dict[str, AbstractDescriptor[typing.Any]], bool]: try: parameters = inspect.signature(callback).parameters.items() except ValueError: # If we can't inspect it then we have to assume this is a NO # As a note, this fails on some "signature-less" builtin functions/types like str. return {}, False descriptors: dict[str, AbstractDescriptor[typing.Any]] = {} for name, parameter in parameters: if parameter.default is parameter.empty or not isinstance(parameter.default, Injected): continue if parameter.kind is parameter.POSITIONAL_ONLY: raise ValueError("Injected positional only arguments are not supported") if parameter.default.callback is not None: descriptors[name] = CallbackDescriptor(parameter.default.callback) else: assert parameter.default.type is not None descriptors[name] = TypeDescriptor(typing.cast("type[_T]", parameter.default.type)) return descriptors, any(d.needs_injector for d in descriptors.values()) def copy(self: _CallbackDescriptorT, *, _new: bool = True) -> _CallbackDescriptorT: """Create a copy of this descriptor. Returns ------- CallbackDescriptor[_T] A copy of this descriptor. """ if not _new: self._callback = copy.copy(self._callback) return self return copy.copy(self).copy(_new=False) def overwrite_callback(self, callback: CallbackSig[_T], /) -> None: """Overwrite the callback of this descriptor. Parameters ---------- callback : CallbackSig[_T] The new callback to overwrite with. Raises ------ ValueError If `callback` has any injected arguments which can only be passed positionally. """ self._callback = callback self._is_async = None self._descriptors, self._needs_injector = self._parse_descriptors(callback) def resolve_with_command_context( self, ctx: tanjun_abc.Context, /, *args: typing.Any, **kwargs: typing.Any ) -> collections.Coroutine[typing.Any, typing.Any, _T]: """Try to resolve the callback with the given command context. Parameters ---------- ctx : tanjun.abc.Context The context to resolve the callback with. *args : typing.Any The positional arguments to pass to the callback. **kwargs : typing.Any The keyword arguments to pass to the callback. Returns ------- _T The callback's result. Raises ------ RuntimeError If the callback needs a dependency injection client but the context does not have one. tanjun.errors.MissingDependencyError If the callback needs an injected type which isn't present in the context or client and doesn't have a set default. """ if self.needs_injector and isinstance(ctx, AbstractInjectionContext): return self.resolve(ctx, *args, **kwargs) return self.resolve_without_injector(*args, **kwargs) def resolve_without_injector( self, *args: typing.Any, **kwargs: typing.Any ) -> collections.Coroutine[typing.Any, typing.Any, _T]: """Try to resolve the callback without a dependency injection client. Parameters ---------- *args : typing.Any The positional arguments to pass to the callback. **kwargs : typing.Any The keyword arguments to pass to the callback. Returns ------- _T The callback's result. Raises ------ RuntimeError If the callback needs a dependency injection client present. """ if self._needs_injector: raise RuntimeError("Callback descriptor needs a dependency injection client") return self.resolve(_EmptyContext(), *args, **kwargs) async def resolve(self, ctx: AbstractInjectionContext, /, *args: typing.Any, **kwargs: typing.Any) -> _T: """Resolve the callback with the given dependency injection context. Parameters ---------- ctx : AbstractInjectionContext The context to resolve the callback with. *args : typing.Any The positional arguments to pass to the callback. **kwargs : typing.Any The keyword arguments to pass to the callback. Returns ------- _T The callback's result. Raises ------ tanjun.errors.MissingDependencyError If the callback needs an injected type which isn't present in the context or client and doesn't have a set default. """ if override := ctx.injection_client.get_callback_override(self._callback): return await override.resolve(ctx, *args, **kwargs) if (result := ctx.get_cached_result(self._callback)) is not UNDEFINED: assert not isinstance(result, Undefined) return result sub_results = {name: await descriptor.resolve(ctx) for name, descriptor in self._descriptors.items()} result = self._callback(*args, **sub_results, **kwargs) if self._is_async is None: self._is_async = inspect.isawaitable(result) if self._is_async: assert inspect.isawaitable(result) result = await result # TODO: should we avoid caching the result if args/kwargs are passed? ctx.cache_result(self._callback, result) return typing.cast(_T, result) class SelfInjectingCallback(CallbackDescriptor[_T]): """Class used to make a callback self-injecting by linking it to a client. Examples -------- ```py async def callback(database: Database = tanjun.inject(type=Database)) -> None: await database.do_something() ... client = tanjun.Client.from_gateway_bot(bot) injecting_callback = tanjun.SelfInjectingCallback(callback, client) await injecting_callback() ``` """ __slots__ = ("_client",) def __init__(self, injector_client: InjectorClient, callback: CallbackSig[_T], /) -> None: """Initialise a self injecting callback. Parameters ---------- injector : InjectorClient The injection client to use to resolve dependencies. callback : CallbackSig[_T] The callback to make self-injecting. Raises ------ ValueError If `callback` has any injected arguments which can only be passed positionally. """ super().__init__(callback) self._client = injector_client async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> _T: """Call this callback with the provided arguments + injected arguments. Parameters ---------- *args : typing.Any The positional arguments to pass to the callback. **kwargs : typing.Any The keyword arguments to pass to the callback. Returns ------- _T The callback's result. """ ctx = BasicInjectionContext(self._client) return await self.resolve(ctx, *args, **kwargs) def as_self_injecting( injector_client: InjectorClient, / ) -> collections.Callable[[CallbackSig[_T]], SelfInjectingCallback[_T]]: """Make a callback self-inecting by linking it to a client through a decorator call. Examples -------- ```py def make_callback(client: tanjun.Client) -> collections.abc.Callable[[], int]: @tanjun.as_self_injected(client) async def get_int_value( redis: redis.Client = tanjun.inject(type=redis.Client) ) -> int: return int(await redis.get('key')) return get_int_value ``` Parameters ---------- injector_client : InjectorClient The injection client to use to resolve dependencies. Returns ------- collections.abc.Callable[[CallbackSig[_T]], SelfInjectingCallback[_T]] """ def decorator(callback: CallbackSig[_T], /) -> SelfInjectingCallback[_T]: return SelfInjectingCallback(injector_client, callback) return decorator if sys.version_info >= (3, 10): _UnionTypes = {typing.Union, types.UnionType} _NoneType = types.NoneType else: _UnionTypes = {typing.Union} _NoneType = type(None) class TypeDescriptor(AbstractDescriptor[_T]): """Descriptor of an injected type. This class holds all the logic for resolving a type with dependency injection. """ __slots__ = ("_default", "_type", "_union") def __init__(self, type_: _TypeT[_T], /) -> None: """Initialise an injected type descriptor. Parameters ---------- type_ : type[_T] The type to resolve. """ self._default: UndefinedOr[_T] = UNDEFINED self._type = type_ self._union: typing.Optional[list[type[_T]]] = None if typing.get_origin(type_) not in _UnionTypes: return sub_types = list(typing.get_args(type_)) try: sub_types.remove(_NoneType) except ValueError: pass else: self._default = typing.cast(_T, None) self._union = sub_types @property def needs_injector(self) -> bool: # <<inherited docstring from AbstractDescriptor>>. return self._default is UNDEFINED @property def type(self) -> _TypeT[_T]: """The type being injected.""" return self._type # type: ignore # pyright bug? def resolve_with_command_context( self, ctx: tanjun_abc.Context, / ) -> collections.Coroutine[typing.Any, typing.Any, _T]: # <<inherited docstring from AbstractDescriptor>>. if self.needs_injector and isinstance(ctx, AbstractInjectionContext): return self.resolve(ctx) return self.resolve_without_injector() async def resolve_without_injector(self) -> _T: # <<inherited docstring from AbstractDescriptor>>. if self._default is not UNDEFINED: assert not isinstance(self._default, Undefined) return self._default raise RuntimeError("Type descriptor cannot be resolved without an injection client") async def resolve(self, ctx: AbstractInjectionContext, /) -> _T: # <<inherited docstring from AbstractDescriptor>>. if (result := ctx.get_type_dependency(self._type)) is not UNDEFINED: assert not isinstance(result, Undefined) return result # We still want to allow for the possibility of a Union being # explicitly implemented so we check types within a union # after the literal type. if self._union: for cls in self._union: if (result := ctx.get_type_dependency(cls)) is not UNDEFINED: assert not isinstance(result, Undefined) return result if self._default is not UNDEFINED: assert not isinstance(self._default, Undefined) return self._default raise errors.MissingDependencyError(f"Couldn't resolve injected type {self._type} to actual value") from None _TypeT = type[_T] class Injected(typing.Generic[_T]): """Decare a keyword-argument as requiring an injected dependency. This is the type returned by `inject`. """ __slots__ = ("callback", "type") def __init__( self, *, callback: typing.Optional[CallbackSig[_T]] = None, type: typing.Optional[_TypeT[_T]] = None, # noqa: A002 ) -> None: # TODO: add default/factory to this? """Initialise an injection default descriptor. Parameters ---------- callback : typing.Optional[CallbackSig[_T]] The callback to use to resolve the dependency. If this callback has no type dependencies then this will still work without an injection context but this can be overridden using `InjectionClient.set_callback_override`. type : typing.Optional[type[_T]] The type of the dependency to resolve. If a union (e.g. `typing.Union[A, B]`, `A | B`, `typing.Optional[A]`) is passed for `type` then each type in the union will be tried separately after the litarl union type is tried, allowing for resolving `A | B` to the value set by `set_type_dependency(B, ...)`. If a union has `None` as one of its types (including `Optional[T]`) then `None` will be passed for the parameter if none of the types could be resolved using the linked client. Raises ------ ValueError If both `callback` and `type` are specified or if neither is specified. """ if callback is None and type is None: raise ValueError("Must specify one of `callback` or `type`") if callback is not None and type is not None: raise ValueError("Only one of `callback` or `type` can be specified") self.callback = callback self.type = type def inject( *, callback: typing.Optional[CallbackSig[_T]] = None, type: typing.Optional[_TypeT[_T]] = None, # noqa: A002 ) -> Injected[_T]: """Decare a keyword-argument as requiring an injected dependency. This should be assigned to an arugment's default value. Examples -------- ```py @tanjun.as_slash_command("name", "description") async def command_callback( ctx: tanjun.abc.Context, # Here we take advantage of scope based special casing which allows # us to inject the `Component` type. injected_type: tanjun.abc.Component = tanjun.inject(type=tanjun.abc.Component) # Here we inject an out-of-scope callback which itself is taking # advantage of type injection. callback_result: ResultT = tanjun.inject(callback=injected_callback) ) -> None: raise NotImplementedError ``` Parameters ---------- callback : typing.Optional[CallbackSig[_T]] The callback to use to resolve the dependency. If this callback has no type dependencies then this will still work without an injection context but this can be overridden using `InjectionClient.set_callback_override`. type : typing.Optional[type[_T]] The type of the dependency to resolve. If a union (e.g. `typing.Union[A, B]`, `A | B`, `typing.Optional[A]`) is passed for `type` then each type in the union will be tried separately after the litarl union type is tried, allowing for resolving `A | B` to the value set by `set_type_dependency(B, ...)`. If a union has `None` as one of its types (including `Optional[T]`) then `None` will be passed for the parameter if none of the types could be resolved using the linked client. Raises ------ ValueError If both `callback` and `type` are specified or if neither is specified. """ return Injected(callback=callback, type=type) def injected( *, callback: typing.Optional[CallbackSig[_T]] = None, type: typing.Optional[_TypeT[_T]] = None, # noqa: A002 ) -> Injected[_T]: """Alias of `inject`.""" return inject(callback=callback, type=type) class InjectorClient: """Dependency injection client used by Tanjun's standard implementation.""" __slots__ = ("_callback_overrides", "_type_dependencies") def __init__(self) -> None: """Initialise an injector client.""" self._callback_overrides: dict[CallbackSig[typing.Any], CallbackDescriptor[typing.Any]] = {} self._type_dependencies: dict[type[typing.Any], typing.Any] = {InjectorClient: self} def set_type_dependency(self: _InjectorClientT, type_: type[_T], value: _T, /) -> _InjectorClientT: """Set a callback to be called to resolve a injected type. Parameters ---------- callback: CallbackSig[_T] The callback to use to resolve the dependency. If this callback has no type dependencies then this will still work without an injection context but this can be overridden using `InjectionClient.set_callback_override`. type_: type[_T] The type of the dependency to resolve. Returns ------- Self The client instance to allow chaining. """ self._type_dependencies[type_] = value return self def get_type_dependency(self, type_: type[_T], /) -> UndefinedOr[_T]: """Get the implementation for an injected type. Parameters ---------- type_: type[_T] The associated type. Returns ------- UndefinedOr[_T] The resolved type if found, else `Undefined`. """ return self._type_dependencies.get(type_, UNDEFINED) def remove_type_dependency(self: _InjectorClientT, type_: type[typing.Any], /) -> _InjectorClientT: """Remove a type dependency. Parameters ---------- type_: type[_T] The associated type. Returns ------- Self The client instance to allow chaining. Raises ------ KeyError If `type_` is not registered. """ del self._type_dependencies[type_] return self def set_callback_override( self: _InjectorClientT, callback: CallbackSig[_T], override: CallbackSig[_T], / ) -> _InjectorClientT: """Override a specific injected callback. .. note:: This does not effect the callbacks set for type injectors. Parameters ---------- callback: CallbackSig[_T] The injected callback to override. override: CallbackSig[_T] The callback to use instead. Returns ------- Self The client instance to allow chaining. """ self._callback_overrides[callback] = CallbackDescriptor(override) return self def get_callback_override(self, callback: CallbackSig[_T], /) -> typing.Optional[CallbackDescriptor[_T]]: """Get the set override for a specific injected callback. Parameters ---------- callback: CallbackSig[_T] The injected callback to get the override for. Returns ------- typing.Optional[CallbackDescriptor[_T]] The override if found, else `None`. """ return self._callback_overrides.get(callback) def remove_callback_override(self: _InjectorClientT, callback: CallbackSig[_T], /) -> _InjectorClientT: """Remove a callback override. Parameters ---------- callback: CallbackSig[_T] The injected callback to remove the override for. Returns ------- Self The client instance to allow chaining. Raises ------ KeyError If no override is found for the callback. """ del self._callback_overrides[callback] return self class _EmptyInjectorClient(InjectorClient): __slots__ = () def set_type_dependency(self: _InjectorClientT, _: type[_T], __: _T, /) -> _InjectorClientT: return self # NOOP is safer here than NotImplementedError def get_type_dependency(self, _: type[typing.Any], /) -> Undefined: return UNDEFINED def remove_type_dependency(self: _InjectorClientT, type_: type[typing.Any], /) -> _InjectorClientT: raise KeyError(type_) def set_callback_override(self: _InjectorClientT, _: CallbackSig[_T], __: CallbackSig[_T], /) -> _InjectorClientT: return self # NOOP is safer here than NotImplementedError def get_callback_override(self, _: CallbackSig[_T], /) -> None: return def remove_callback_override(self: _InjectorClientT, callback: CallbackSig[_T], /) -> _InjectorClientT: raise KeyError(callback) _EMPTY_CLIENT = _EmptyInjectorClient() class _EmptyContext(AbstractInjectionContext): __slots__ = ("_result_cache",) def __init__(self) -> None: self._result_cache: typing.Optional[dict[CallbackSig[typing.Any], typing.Any]] = None @property def injection_client(self) -> InjectorClient: return _EMPTY_CLIENT def cache_result(self, callback: CallbackSig[_T], value: _T, /) -> None: if self._result_cache is None: self._result_cache = {} self._result_cache[callback] = value def get_cached_result(self, callback: CallbackSig[typing.Any], /) -> Undefined: return self._result_cache.get(callback, UNDEFINED) if self._result_cache else UNDEFINED def get_type_dependency(self, _: type[typing.Any], /) -> Undefined: return UNDEFINED
Logic and data classes used within the standard Tanjun implementation to enable dependency injection.
View Source
def as_self_injecting( injector_client: InjectorClient, / ) -> collections.Callable[[CallbackSig[_T]], SelfInjectingCallback[_T]]: """Make a callback self-inecting by linking it to a client through a decorator call. Examples -------- ```py def make_callback(client: tanjun.Client) -> collections.abc.Callable[[], int]: @tanjun.as_self_injected(client) async def get_int_value( redis: redis.Client = tanjun.inject(type=redis.Client) ) -> int: return int(await redis.get('key')) return get_int_value ``` Parameters ---------- injector_client : InjectorClient The injection client to use to resolve dependencies. Returns ------- collections.abc.Callable[[CallbackSig[_T]], SelfInjectingCallback[_T]] """ def decorator(callback: CallbackSig[_T], /) -> SelfInjectingCallback[_T]: return SelfInjectingCallback(injector_client, callback) return decorator
Make a callback self-inecting by linking it to a client through a decorator call.
Examples
def make_callback(client: tanjun.Client) -> collections.abc.Callable[[], int]:
@tanjun.as_self_injected(client)
async def get_int_value(
redis: redis.Client = tanjun.inject(type=redis.Client)
) -> int:
return int(await redis.get('key'))
return get_int_value
Parameters
- injector_client (InjectorClient): The injection client to use to resolve dependencies.
Returns
- collections.abc.Callable[[CallbackSig[_T]], SelfInjectingCallback[_T]]
View Source
def inject( *, callback: typing.Optional[CallbackSig[_T]] = None, type: typing.Optional[_TypeT[_T]] = None, # noqa: A002 ) -> Injected[_T]: """Decare a keyword-argument as requiring an injected dependency. This should be assigned to an arugment's default value. Examples -------- ```py @tanjun.as_slash_command("name", "description") async def command_callback( ctx: tanjun.abc.Context, # Here we take advantage of scope based special casing which allows # us to inject the `Component` type. injected_type: tanjun.abc.Component = tanjun.inject(type=tanjun.abc.Component) # Here we inject an out-of-scope callback which itself is taking # advantage of type injection. callback_result: ResultT = tanjun.inject(callback=injected_callback) ) -> None: raise NotImplementedError ``` Parameters ---------- callback : typing.Optional[CallbackSig[_T]] The callback to use to resolve the dependency. If this callback has no type dependencies then this will still work without an injection context but this can be overridden using `InjectionClient.set_callback_override`. type : typing.Optional[type[_T]] The type of the dependency to resolve. If a union (e.g. `typing.Union[A, B]`, `A | B`, `typing.Optional[A]`) is passed for `type` then each type in the union will be tried separately after the litarl union type is tried, allowing for resolving `A | B` to the value set by `set_type_dependency(B, ...)`. If a union has `None` as one of its types (including `Optional[T]`) then `None` will be passed for the parameter if none of the types could be resolved using the linked client. Raises ------ ValueError If both `callback` and `type` are specified or if neither is specified. """ return Injected(callback=callback, type=type)
Decare a keyword-argument as requiring an injected dependency.
This should be assigned to an arugment's default value.
Examples
@tanjun.as_slash_command("name", "description")
async def command_callback(
ctx: tanjun.abc.Context,
# Here we take advantage of scope based special casing which allows
# us to inject the `Component` type.
injected_type: tanjun.abc.Component = tanjun.inject(type=tanjun.abc.Component)
# Here we inject an out-of-scope callback which itself is taking
# advantage of type injection.
callback_result: ResultT = tanjun.inject(callback=injected_callback)
) -> None:
raise NotImplementedError
Parameters
callback (typing.Optional[CallbackSig[_T]]): The callback to use to resolve the dependency.
If this callback has no type dependencies then this will still work without an injection context but this can be overridden using
InjectionClient.set_callback_override.type (typing.Optional[type[_T]]): The type of the dependency to resolve.
If a union (e.g.
typing.Union[A, B],A | B,typing.Optional[A]) is passed fortypethen each type in the union will be tried separately after the litarl union type is tried, allowing for resolvingA | Bto the value set byset_type_dependency(B, ...).If a union has
Noneas one of its types (includingOptional[T]) thenNonewill be passed for the parameter if none of the types could be resolved using the linked client.
Raises
- ValueError: If both
callbackandtypeare specified or if neither is specified.
View Source
def injected( *, callback: typing.Optional[CallbackSig[_T]] = None, type: typing.Optional[_TypeT[_T]] = None, # noqa: A002 ) -> Injected[_T]: """Alias of `inject`.""" return inject(callback=callback, type=type)
Alias of inject.
View Source
# -*- coding: utf-8 -*- # cython: language_level=3 # BSD 3-Clause License # # Copyright (c) 2020-2022, Faster Speeding # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Standard implementation of message command argument parsing.""" from __future__ import annotations __all__: list[str] = [ "AbstractOptionParser", "Argument", "ConverterSig", "Option", "Parameter", "ShlexParser", "UndefinedT", "UNDEFINED", "with_argument", "with_greedy_argument", "with_multi_argument", "with_option", "with_multi_option", "with_parser", ] import abc import asyncio import copy import itertools import shlex import typing from collections import abc as collections from . import abc as tanjun_abc from . import conversion from . import errors from . import injecting if typing.TYPE_CHECKING: _CommandT = typing.TypeVar("_CommandT", bound=tanjun_abc.MessageCommand[typing.Any]) _ParameterT = typing.TypeVar("_ParameterT", bound="Parameter") _ShlexParserT = typing.TypeVar("_ShlexParserT", bound="ShlexParser") _T_contra = typing.TypeVar("_T_contra", contravariant=True) _OtherT = typing.TypeVar("_OtherT") class _CmpProto(typing.Protocol[_T_contra]): def __gt__(self, __other: _T_contra) -> bool: raise NotImplementedError def __lt__(self, __other: _T_contra) -> bool: raise NotImplementedError _CmpProtoT = typing.TypeVar("_CmpProtoT", bound=_CmpProto[typing.Any]) _T = typing.TypeVar("_T") ConverterSig = collections.Callable[..., tanjun_abc.MaybeAwaitableT[_T]] """Type hint of a converter used within a parser instance. This must be a callable or asynchronous callable which takes one position `str`, argument and returns the resultant value. """ _MaybeIterable = typing.Union[collections.Iterable[_T], _T] class UndefinedT: """Singleton used to indicate an undefined value within parsing logic.""" __singleton: typing.Optional[UndefinedT] = None def __new__(cls) -> UndefinedT: if cls.__singleton is None: cls.__singleton = super().__new__(cls) assert isinstance(cls.__singleton, UndefinedT) return cls.__singleton def __repr__(self) -> str: return "UNDEFINED" def __bool__(self) -> typing.Literal[False]: return False UndefinedDefaultT = UndefinedT """Deprecated alias of `UndefinedT`.""" UNDEFINED = UndefinedT() """A singleton used to represent an undefined value within parsing logic.""" UNDEFINED_DEFAULT = UNDEFINED """Deprecated alias of `UNDEFINED`.""" _UndefinedOr = typing.Union[UndefinedT, _T] class AbstractOptionParser(tanjun_abc.MessageParser, abc.ABC): """Abstract interface of a message content parser.""" __slots__ = () @property @abc.abstractmethod def arguments(self) -> collections.Sequence[Argument]: """Sequence of the positional arguments registered with this parser.""" @property @abc.abstractmethod def options(self) -> collections.Sequence[Option]: """Sequence of the named options registered with this parser.""" @typing.overload @abc.abstractmethod def add_argument( self: _T, key: str, /, *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, multi: bool = False, ) -> _T: ... @typing.overload @abc.abstractmethod def add_argument( self: _T, key: str, /, converters: _MaybeIterable[ConverterSig[_CmpProtoT]], *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, multi: bool = False, ) -> _T: ... @typing.overload @abc.abstractmethod def add_argument( self: _T, key: str, /, converters: _MaybeIterable[ConverterSig[_OtherT]], *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProto[_OtherT]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[_OtherT]] = UNDEFINED, multi: bool = False, ) -> _T: ... @typing.overload @abc.abstractmethod def add_argument( self: _T, key: str, /, converters: _MaybeIterable[ConverterSig[typing.Any]], *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, multi: bool = False, ) -> _T: ... @abc.abstractmethod def add_argument( self: _T, key: str, /, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, multi: bool = False, ) -> _T: """Add a positional argument type to the parser.. .. note:: Order matters for positional arguments. Parameters ---------- key : str The string identifier of this argument (may be used to pass the result of this argument to the command's callback during execution). Other Parameters ---------------- converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]] The converter(s) this argument should use to handle values passed to it during parsing. If no converters are provided then the raw string value will be passed. Only the first converter to pass will be used. default : typing.Any The default value of this argument, if left as `UNDEFINED` then this will have no default. greedy : bool Whether or not this argument should be greedy (meaning that it takes in the remaining argument values). max_value Assert that the parsed value(s) for this argument are less than or equal to this. If any converters are provided then this should be compatible with the result of them. min_value Assert that the parsed value(s) for this argument are greater than or equal to this. If any converters are provided then this should be compatible with the result of them. multi : bool Whether this argument can be passed multiple times. Returns ------- Self This parser to enable chained calls. """ @typing.overload @abc.abstractmethod def add_option( self: _T, key: str, name: str, /, *names: str, default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, multi: bool = False, ) -> _T: ... @typing.overload @abc.abstractmethod def add_option( self: _T, key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[_CmpProtoT]], default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, multi: bool = False, ) -> _T: ... @typing.overload @abc.abstractmethod def add_option( self: _T, key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[_OtherT]], default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[_OtherT]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[_OtherT]] = UNDEFINED, multi: bool = False, ) -> _T: ... @typing.overload @abc.abstractmethod def add_option( self: _T, key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[typing.Any]], default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, multi: bool = False, ) -> _T: ... @abc.abstractmethod def add_option( self: _T, key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, multi: bool = False, ) -> _T: """Add an named option to this parser. Parameters ---------- key : str The string identifier of this option which will be used to pass the result of this option to the command's callback during execution as a keyword argument. name : str The name of this option used for identifying it in the parsed content. default : typing.Any The default value of this option, unlike arguments this is required for options. Other Parameters ---------------- *names : str Other names of this option used for identifying it in the parsed content. converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]] The converter(s) this option should use to handle values passed to it during parsing. If no converters are provided then the raw string value will be passed. Only the first converter to pass will be used. empty_value : typing.Any The value to use if this option is provided without a value. If left as `UNDEFINED` then this option will error if it's provided without a value. max_value Assert that the parsed value(s) for this option are less than or equal to this. If any converters are provided then this should be compatible with the result of them. min_value Assert that the parsed value(s) for this option are greater than or equal to this. If any converters are provided then this should be compatible with the result of them. multi : bool If this option can be provided multiple times. Defaults to `False`. Returns ------- Self This parser to enable chained calls. """ AbstractParser = AbstractOptionParser """Deprecated alias of `AbstractOptionParser`.""" class _ShlexTokenizer: __slots__ = ("__arg_buffer", "__last_name", "__options_buffer", "__shlex") def __init__(self, content: str, /) -> None: self.__arg_buffer: list[str] = [] self.__last_name: typing.Optional[str] = None self.__options_buffer: list[tuple[str, typing.Optional[str]]] = [] self.__shlex = shlex.shlex(content, posix=True) self.__shlex.commenters = "" self.__shlex.quotes = '"' self.__shlex.whitespace = " " self.__shlex.whitespace_split = True def collect_raw_options(self) -> collections.Mapping[str, collections.Sequence[typing.Optional[str]]]: results: dict[str, list[typing.Optional[str]]] = {} while (option := self.next_raw_option()) is not None: name, value = option if name not in results: results[name] = [] results[name].append(value) return results def iter_raw_arguments(self) -> collections.Iterator[str]: while (argument := self.next_raw_argument()) is not None: yield argument def next_raw_argument(self) -> typing.Optional[str]: if self.__arg_buffer: return self.__arg_buffer.pop(0) while (value := self.__seek_shlex()) and value[0] == 1: self.__options_buffer.append(value[1]) return value[1] if value else None def next_raw_option(self) -> typing.Optional[tuple[str, typing.Optional[str]]]: if self.__options_buffer: return self.__options_buffer.pop(0) while (value := self.__seek_shlex()) and value[0] == 0: self.__arg_buffer.append(value[1]) return value[1] if value else None def __seek_shlex( self, ) -> typing.Union[tuple[typing.Literal[0], str], tuple[typing.Literal[1], tuple[str, typing.Optional[str]]], None]: option_name = self.__last_name try: value = next(self.__shlex) except StopIteration: if option_name is not None: self.__last_name = None return (1, (option_name, None)) return None except ValueError as exc: raise errors.ParserError(str(exc), None) from exc is_option = value.startswith("-") if is_option and option_name is not None: self.__last_name = value return (1, (option_name, None)) if is_option: self.__last_name = value return self.__seek_shlex() if option_name: self.__last_name = None return (1, (option_name, value)) return (0, value) async def _covert_option_or_empty( ctx: tanjun_abc.MessageContext, option: Option, value: typing.Optional[typing.Any], / ) -> typing.Any: if value is not None: return await option.convert(ctx, value) if option.empty_value is not UNDEFINED: return option.empty_value raise errors.NotEnoughArgumentsError(f"Option '{option.key} cannot be empty.", option.key) class _SemanticShlex(_ShlexTokenizer): __slots__ = ("__arguments", "__ctx", "__options") def __init__( self, ctx: tanjun_abc.MessageContext, arguments: collections.Sequence[Argument], options: collections.Sequence[Option], /, ) -> None: super().__init__(ctx.content) self.__arguments = arguments self.__ctx = ctx self.__options = options async def parse(self) -> dict[str, typing.Any]: raw_options = self.collect_raw_options() results = asyncio.gather(*map(lambda option: self.__process_option(option, raw_options), self.__options)) values = dict(zip((option.key for option in self.__options), await results)) for argument in self.__arguments: values[argument.key] = await self.__process_argument(argument) if argument.is_greedy or argument.is_multi: break # Multi and Greedy parameters should always be the last parameter. return values async def __process_argument(self, argument: Argument) -> typing.Any: if argument.is_greedy and (value := " ".join(self.iter_raw_arguments())): return await argument.convert(self.__ctx, value) if argument.is_multi and (values := list(self.iter_raw_arguments())): return await asyncio.gather(*(argument.convert(self.__ctx, value) for value in values)) # If the previous two statements failed on getting raw arguments then this will as well. if (optional_value := self.next_raw_argument()) is not None: return await argument.convert(self.__ctx, optional_value) if argument.default is not UNDEFINED: return argument.default # If this is reached then no value was found. raise errors.NotEnoughArgumentsError(f"Missing value for required argument '{argument.key}'", argument.key) async def __process_option( self, option: Option, raw_options: collections.Mapping[str, collections.Sequence[typing.Optional[str]]] ) -> typing.Any: values_iter = itertools.chain.from_iterable(raw_options[name] for name in option.names if name in raw_options) if option.is_multi and (values := list(values_iter)): return await asyncio.gather(*(_covert_option_or_empty(self.__ctx, option, value) for value in values)) if not option.is_multi and (value := next(values_iter, ...)) is not ...: if next(values_iter, ...) is not ...: raise errors.TooManyArgumentsError(f"Option `{option.key}` can only take a single value", option.key) return await _covert_option_or_empty(self.__ctx, option, value) if option.default is not UNDEFINED: return option.default # If this is reached then no value was found. raise errors.NotEnoughArgumentsError(f"Missing required option `{option.key}`", option.key) def _get_or_set_parser(command: tanjun_abc.MessageCommand[typing.Any], /) -> AbstractOptionParser: if not command.parser: parser = ShlexParser() command.set_parser(parser) return parser if isinstance(command.parser, AbstractOptionParser): return command.parser raise TypeError("Expected parser to be an instance of tanjun.parsing.AbstractOptionParser") @typing.overload def with_argument( key: str, /, *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, multi: bool = False, ) -> collections.Callable[[_CommandT], _CommandT]: ... @typing.overload def with_argument( key: str, /, converters: _MaybeIterable[ConverterSig[_CmpProtoT]], *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, multi: bool = False, ) -> collections.Callable[[_CommandT], _CommandT]: ... @typing.overload def with_argument( key: str, /, converters: _MaybeIterable[ConverterSig[_T]], *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED, multi: bool = False, ) -> collections.Callable[[_CommandT], _CommandT]: ... @typing.overload def with_argument( key: str, /, converters: _MaybeIterable[ConverterSig[typing.Any]], *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, multi: bool = False, ) -> collections.Callable[[_CommandT], _CommandT]: ... def with_argument( key: str, /, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, multi: bool = False, ) -> collections.Callable[[_CommandT], _CommandT]: """Add an argument to a message command through a decorator call. Notes ----- * Order matters for positional arguments and since decorator execution starts at the decorator closest to the command and goes upwards this will decide where a positional argument is located in a command's signature. * If no parser is explicitly set on the command this is decorating before this decorator call then this will set `ShlexParser` as the parser. Parameters ---------- key : str The string identifier of this argument (may be used to pass the result of this argument to the command's callback during execution). converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]] The converter(s) this argument should use to handle values passed to it during parsing. If no converters are provided then the raw string value will be passed. Only the first converter to pass will be used. default : typing.Any The default value of this argument, if left as `UNDEFINED` then this will have no default. greedy : bool Whether or not this argument should be greedy (meaning that it takes in the remaining argument values). max_value Assert that the parsed value(s) for this argument are less than or equal to this. If any converters are provided then this should be compatible with the result of them. min_value Assert that the parsed value(s) for this argument are greater than or equal to this. If any converters are provided then this should be compatible with the result of them. multi : bool Whether this argument can be passed multiple times. Returns ------- collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]: Decorator function for the message command this argument is being added to. Examples -------- ```python import tanjun @tanjun.parsing.with_argument("command", converters=int, default=42) @tanjun.parsing.with_parser @tanjun.component.as_message_command("command") async def command(self, ctx: tanjun.abc.Context, /, argument: int): ... ``` """ def decorator(command: _CommandT, /) -> _CommandT: _get_or_set_parser(command).add_argument( key, converters=converters, default=default, greedy=greedy, max_value=max_value, min_value=min_value, multi=multi, ) return command return decorator @typing.overload def with_greedy_argument( key: str, /, *, default: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, ) -> collections.Callable[[_CommandT], _CommandT]: ... @typing.overload def with_greedy_argument( key: str, /, converters: _MaybeIterable[ConverterSig[_CmpProtoT]], *, default: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, ) -> collections.Callable[[_CommandT], _CommandT]: ... @typing.overload def with_greedy_argument( key: str, /, converters: _MaybeIterable[ConverterSig[_T]], *, default: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED, ) -> collections.Callable[[_CommandT], _CommandT]: ... @typing.overload def with_greedy_argument( key: str, /, converters: _MaybeIterable[ConverterSig[typing.Any]], *, default: _UndefinedOr[typing.Any] = UNDEFINED, ) -> collections.Callable[[_CommandT], _CommandT]: ... def with_greedy_argument( key: str, /, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), *, default: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, ) -> collections.Callable[[_CommandT], _CommandT]: """Add a greedy argument to a message command through a decorator call. Notes ----- * A greedy argument will consume the remaining positional arguments and pass them through to the converters as one joined string while also requiring that at least one more positional argument is remaining unless a default is set. * Order matters for positional arguments and since decorator execution starts at the decorator closest to the command and goes upwards this will decide where a positional argument is located in a command's signature. * If no parser is explicitly set on the command this is decorating before this decorator call then this will set `ShlexParser` as the parser. Parameters ---------- key : str The string identifier of this argument (may be used to pass the result of this argument to the command's callback during execution). Other Parameters ---------------- converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]] The converter(s) this argument should use to handle values passed to it during parsing. If no converters are provided then the raw string value will be passed. Only the first converter to pass will be used. default : typing.Any The default value of this argument, if left as `UNDEFINED` then this will have no default. max_value Assert that the parsed value(s) for this argument are less than or equal to this. If any converters are provided then this should be compatible with the result of them. min_value Assert that the parsed value(s) for this argument are greater than or equal to this. If any converters are provided then this should be compatible with the result of them. Returns ------- collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]: Decorator function for the message command this argument is being added to. Examples -------- ```python import tanjun @tanjun.parsing.with_greedy_argument("command", converters=StringView) @tanjun.parsing.with_parser @tanjun.component.as_message_command("command") async def command(self, ctx: tanjun.abc.Context, /, argument: StringView): ... ``` """ return with_argument( key, converters=converters, default=default, greedy=True, max_value=max_value, min_value=min_value ) @typing.overload def with_multi_argument( key: str, /, *, default: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, ) -> collections.Callable[[_CommandT], _CommandT]: ... @typing.overload def with_multi_argument( key: str, /, converters: _MaybeIterable[ConverterSig[_CmpProtoT]], *, default: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, ) -> collections.Callable[[_CommandT], _CommandT]: ... @typing.overload def with_multi_argument( key: str, /, converters: _MaybeIterable[ConverterSig[_T]], *, default: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED, ) -> collections.Callable[[_CommandT], _CommandT]: ... @typing.overload def with_multi_argument( key: str, /, converters: _MaybeIterable[ConverterSig[typing.Any]], *, default: _UndefinedOr[typing.Any] = UNDEFINED, ) -> collections.Callable[[_CommandT], _CommandT]: ... def with_multi_argument( key: str, /, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), *, default: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, ) -> collections.Callable[[_CommandT], _CommandT]: """Add a multi-argument to a message command through a decorator call. Notes ----- * A multi argument will consume the remaining positional arguments and pass them to the converters through multiple calls while also requiring that at least one more positional argument is remaining unless a default is set and passing through the results to the command's callback as a sequence. * Order matters for positional arguments and since decorator execution starts at the decorator closest to the command and goes upwards this will decide where a positional argument is located in a command's signature. * If no parser is explicitly set on the command this is decorating before this decorator call then this will set `ShlexParser` as the parser. Parameters ---------- key : str The string identifier of this argument (may be used to pass the result of this argument to the command's callback during execution). Other Parameters ---------------- converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]] The converter(s) this argument should use to handle values passed to it during parsing. If no converters are provided then the raw string value will be passed. Only the first converter to pass will be used. default : typing.Any The default value of this argument, if left as `UNDEFINED` then this will have no default. max_value Assert that the parsed value(s) for this argument are less than or equal to this. If any converters are provided then this should be compatible with the result of them. min_value Assert that the parsed value(s) for this argument are greater than or equal to this. If any converters are provided then this should be compatible with the result of them. Returns ------- collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]: Decorator function for the message command this argument is being added to. Examples -------- ```python import tanjun @tanjun.parsing.with_multi_argument("command", converters=int) @tanjun.parsing.with_parser @tanjun.component.as_message_command("command") async def command(self, ctx: tanjun.abc.Context, /, argument: collections.abc.Sequence[int]): ... ``` """ return with_argument( key, converters=converters, default=default, max_value=max_value, min_value=min_value, multi=True ) @typing.overload def with_option( key: str, name: str, /, *names: str, default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, multi: bool = False, ) -> collections.Callable[[_CommandT], _CommandT]: ... @typing.overload def with_option( key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[_CmpProtoT]], default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, multi: bool = False, ) -> collections.Callable[[_CommandT], _CommandT]: ... @typing.overload def with_option( key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[_T]], default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED, multi: bool = False, ) -> collections.Callable[[_CommandT], _CommandT]: ... @typing.overload def with_option( key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[typing.Any]], default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, multi: bool = False, ) -> collections.Callable[[_CommandT], _CommandT]: ... # TODO: add default getter def with_option( key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, multi: bool = False, ) -> collections.Callable[[_CommandT], _CommandT]: """Add an option to a message command through a decorator call. .. note:: If no parser is explicitly set on the command this is decorating before this decorator call then this will set `ShlexParser` as the parser. Parameters ---------- key : str The string identifier of this option which will be used to pass the result of this argument to the command's callback during execution as a keyword argument. name : str The name of this option used for identifying it in the parsed content. default : typing.Any The default value of this argument, unlike arguments this is required for options. Other Parameters ---------------- *names : str Other names of this option used for identifying it in the parsed content. converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]] The converter(s) this argument should use to handle values passed to it during parsing. If no converters are provided then the raw string value will be passed. Only the first converter to pass will be used. empty_value : typing.Any The value to use if this option is provided without a value. If left as `UNDEFINED` then this option will error if it's provided without a value. max_value Assert that the parsed value(s) for this option are less than or equal to this. If any converters are provided then this should be compatible with the result of them. min_value Assert that the parsed value(s) for this option are greater than or equal to this. If any converters are provided then this should be compatible with the result of them. multi : bool If this option can be provided multiple times. Defaults to `False`. Returns ------- collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]: Decorator function for the message command this option is being added to. Examples -------- ```python import tanjun @tanjun.parsing.with_option("command", converters=int, default=42) @tanjun.parsing.with_parser @tanjun.component.as_message_command("command") async def command(self, ctx: tanjun.abc.Context, /, argument: int): ... ``` """ def decorator(command: _CommandT, /) -> _CommandT: _get_or_set_parser(command).add_option( key, name, *names, converters=converters, default=default, empty_value=empty_value, max_value=max_value, min_value=min_value, multi=multi, ) return command return decorator @typing.overload def with_multi_option( key: str, name: str, /, *names: str, default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, ) -> collections.Callable[[_CommandT], _CommandT]: ... @typing.overload def with_multi_option( key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[_CmpProtoT]], default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, ) -> collections.Callable[[_CommandT], _CommandT]: ... @typing.overload def with_multi_option( key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[_T]], default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED, ) -> collections.Callable[[_CommandT], _CommandT]: ... @typing.overload def with_multi_option( key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[typing.Any]], default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, ) -> collections.Callable[[_CommandT], _CommandT]: ... def with_multi_option( key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, ) -> collections.Callable[[_CommandT], _CommandT]: """Add an multi-option to a command's parser through a decorator call. Notes ----- * A multi option will consume all the values provided for an option and pass them through to the converters as an array of strings while also requiring that at least one value is provided for the option unless a default is set. * If no parser is explicitly set on the command this is decorating before this decorator call then this will set `ShlexParser` as the parser. Parameters ---------- key : str The string identifier of this option which will be used to pass the result of this argument to the command's callback during execution as a keyword argument. name : str The name of this option used for identifying it in the parsed content. default : typing.Any The default value of this argument, unlike arguments this is required for options. Other Parameters ---------------- *names : str Other names of this option used for identifying it in the parsed content. converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]] The converter(s) this argument should use to handle values passed to it during parsing. If no converters are provided then the raw string value will be passed. Only the first converter to pass will be used. empty_value : typing.Any The value to use if this option is provided without a value. If left as `UNDEFINED` then this option will error if it's provided without a value. max_value Assert that the parsed value(s) for this option are less than or equal to this. If any converters are provided then this should be compatible with the result of them. min_value Assert that the parsed value(s) for this option are greater than or equal to this. If any converters are provided then this should be compatible with the result of them. Returns ------- collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]: Decorator function for the message command this option is being added to. Examples -------- ```python import tanjun @tanjun.parsing.with_multi_option("command", converters=int, default=()) @tanjun.parsing.with_parser @tanjun.component.as_message_command("command") async def command(self, ctx: tanjun.abc.Context, /, argument: collections.abc.Sequence[int]): ... ``` """ return with_option( key, name, *names, converters=converters, default=default, empty_value=empty_value, max_value=max_value, min_value=min_value, multi=True, ) class Parameter: """Base class for parameters for the standard parser(s).""" __slots__ = ("_client", "_component", "_converters", "_default", "_is_multi", "_key", "_max_value", "_min_value") def __init__( self, key: str, /, *, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), default: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, multi: bool = False, ) -> None: """Initialise a parameter.""" self._client: typing.Optional[tanjun_abc.Client] = None self._component: typing.Optional[tanjun_abc.Component] = None self._converters: list[injecting.CallbackDescriptor[typing.Any]] = [] self._default = default self._is_multi = multi self._key = key self._max_value = max_value self._min_value = min_value if key.startswith("-"): raise ValueError("parameter key cannot start with `-`") if isinstance(converters, collections.Iterable): for converter in converters: self._add_converter(converter) else: self._add_converter(converters) def __repr__(self) -> str: return f"{type(self).__name__} <{self._key}>" @property def converters(self) -> collections.Sequence[ConverterSig[typing.Any]]: """Sequence of the converters registered for this parameter.""" return tuple(converter.callback for converter in self._converters) @property def default(self) -> _UndefinedOr[typing.Any]: """The parameter's default. If this is `UndefinedT` then this parameter is required. """ return self._default @property def is_multi(self) -> bool: """Whether this parameter is "multi". Multi parameters will be passed a list of all the values provided for this parameter (with each entry being converted separately.) """ return self._is_multi @property def key(self) -> str: """The key of this parameter used to pass the result to the command's callback.""" return self._key @property def needs_injector(self) -> bool: """Whether this parameter needs an injector to be used.""" # TODO: cache this value? return any(converter.needs_injector for converter in self._converters) def _add_converter(self, converter: ConverterSig[typing.Any], /) -> None: if isinstance(converter, conversion.BaseConverter): if self._client: converter.check_client(self._client, f"{self._key} parameter") if not isinstance(converter, injecting.CallbackDescriptor): # Some types like `bool` and `bytes` are overridden here for the sake of convenience. converter = conversion.override_type(converter) converter_ = injecting.CallbackDescriptor(converter) self._converters.append(converter_) else: self._converters.append(converter) def bind_client(self, client: tanjun_abc.Client, /) -> None: self._client = client for converter in self._converters: if isinstance(converter.callback, conversion.BaseConverter): converter.callback.check_client(client, f"{self._key} parameter") def bind_component(self, component: tanjun_abc.Component, /) -> None: self._component = component def _validate(self, value: typing.Any, /) -> None: # assert value >= self._min_value if self._min_value is not UNDEFINED and self._min_value > value: raise errors.ConversionError( f"{self._key!r} must be greater than or equal to {self._min_value!r}", self.key ) # assert value <= self._max_value if self._max_value is not UNDEFINED and self._max_value < value: raise errors.ConversionError(f"{self._key!r} must be less than or equal to {self._max_value!r}", self.key) async def convert(self, ctx: tanjun_abc.Context, value: str) -> typing.Any: """Convert the given value to the type of this parameter.""" if not self._converters: self._validate(value) return value sources: list[ValueError] = [] for converter in self._converters: try: result = await converter.resolve_with_command_context(ctx, value) except ValueError as exc: sources.append(exc) else: self._validate(result) return result parameter_type = "option" if isinstance(self, Option) else "argument" raise errors.ConversionError(f"Couldn't convert {parameter_type} '{self.key}'", self.key, sources) def copy(self: _ParameterT, *, _new: bool = True) -> _ParameterT: """Copy the parameter. Returns ------- Self A copy of the parameter. """ if not _new: self._converters = [converter.copy() for converter in self._converters] return self result = copy.copy(self).copy(_new=False) return result class Argument(Parameter): """Representation of a positional argument used by the standard parser.""" __slots__ = ("_is_greedy",) def __init__( self, key: str, /, *, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, multi: bool = False, ) -> None: """Initialise a positional argument. Parameters ---------- key : str The string identifier of this argument (may be used to pass the result of this argument to the command's callback during execution). Other Parameters ---------------- converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]] The converter(s) this argument should use to handle values passed to it during parsing. If no converters are provided then the raw string value will be passed. Only the first converter to pass will be used. default : typing.Any The default value of this argument, if left as `UNDEFINED` then this will have no default. greedy : bool Whether or not this argument should be greedy (meaning that it takes in the remaining argument values). max_value Assert that the parsed value(s) for this option are less than or equal to this. If any converters are provided then this should be compatible with the result of them. min_value Assert that the parsed value(s) for this option are greater than or equal to this. If any converters are provided then this should be compatible with the result of them. multi : bool Whether this argument can be passed multiple times. """ if greedy and multi: raise ValueError("Argument cannot be both greed and multi.") self._is_greedy = greedy super().__init__( key, converters=converters, default=default, max_value=max_value, min_value=min_value, multi=multi ) @property def is_greedy(self) -> bool: """Whether this parameter is greedy. Greedy parameters will consume the remaining message content as one string (with converters also being passed the whole string). .. note:: Greedy and multi parameters cannot be used together. """ return self._is_greedy class Option(Parameter): """Representation of a named optional parameter used by the standard parser.""" __slots__ = ("_empty_value", "_names") def __init__( self, key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), default: _UndefinedOr[typing.Any] = UNDEFINED, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, multi: bool = True, ) -> None: """Initialise a named optional parameter. Parameters ---------- key : str The string identifier of this option which will be used to pass the result of this argument to the command's callback during execution as a keyword argument. name : str The name of this option used for identifying it in the parsed content. default : typing.Any The default value of this argument, unlike arguments this is required for options. Other Parameters ---------------- *names : str Other names of this option used for identifying it in the parsed content. converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]] The converter(s) this argument should use to handle values passed to it during parsing. If no converters are provided then the raw string value will be passed. Only the first converter to pass will be used. empty_value : typing.Any The value to use if this option is provided without a value. If left as `UNDEFINED` then this option will error if it's provided without a value. max_value Assert that the parsed value(s) for this option are less than or equal to this. If any converters are provided then this should be compatible with the result of them. min_value Assert that the parsed value(s) for this option are greater than or equal to this. If any converters are provided then this should be compatible with the result of them. multi : bool If this option can be provided multiple times. Defaults to `False`. """ if not name.startswith("-") or not all(n.startswith("-") for n in names): raise ValueError("All option names must start with `-`") self._empty_value = empty_value self._names = [name, *names] super().__init__( key, converters=converters, default=default, max_value=max_value, min_value=min_value, multi=multi ) @property def empty_value(self) -> _UndefinedOr[typing.Any]: """The value to return if the option is empty. If this is `UndefinedT` then a value will be required for the option. """ return self._empty_value @property def names(self) -> collections.Sequence[str]: """Sequence of the CLI names of this option.""" return self._names.copy() def __repr__(self) -> str: return f"{type(self).__name__} <{self.key}, {self._names}>" class ShlexParser(AbstractOptionParser): """A shlex based `AbstractOptionParser` implementation.""" __slots__ = ("_arguments", "_client", "_component", "_options") def __init__(self) -> None: """Initialise a shlex parser.""" self._arguments: list[Argument] = [] self._client: typing.Optional[tanjun_abc.Client] = None self._component: typing.Optional[tanjun_abc.Component] = None self._options: list[Option] = [] # TODO: maybe switch to dict[str, Option] and assert doesn't already exist @property def needs_injector(self) -> bool: """Whether this parser needs an injector to be used.""" # TODO: cache this value? return any(parameter.needs_injector for parameter in itertools.chain(self._options, self._arguments)) @property def arguments(self) -> collections.Sequence[Argument]: # <<inherited docstring from AbstractOptionParser>>. return self._arguments.copy() @property def options(self) -> collections.Sequence[Option]: # <<inherited docstring from AbstractOptionParser>>. return self._options.copy() def copy(self: _ShlexParserT, *, _new: bool = True) -> _ShlexParserT: # <<inherited docstring from AbstractOptionParser>>. if not _new: self._arguments = [argument.copy() for argument in self._arguments] self._options = [option.copy() for option in self._options] return self return copy.copy(self).copy(_new=False) @typing.overload def add_argument( self: _ShlexParserT, key: str, /, *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: ... @typing.overload def add_argument( self: _ShlexParserT, key: str, /, converters: _MaybeIterable[ConverterSig[_CmpProtoT]], *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: ... @typing.overload def add_argument( self: _ShlexParserT, key: str, /, converters: _MaybeIterable[ConverterSig[_T]], *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: ... @typing.overload def add_argument( self: _ShlexParserT, key: str, /, converters: _MaybeIterable[ConverterSig[typing.Any]], *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, multi: bool = False, ) -> _ShlexParserT: ... def add_argument( self: _ShlexParserT, key: str, /, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: # <<inherited docstring from AbstractOptionParser>>. argument = Argument( key, converters=converters, default=default, greedy=greedy, max_value=max_value, min_value=min_value, multi=multi, ) if self._client: argument.bind_client(self._client) if self._component: argument.bind_component(self._component) found_final_argument = False for argument in self._arguments: if found_final_argument: del self._arguments[-1] raise ValueError("Multi or greedy argument must be the last argument") found_final_argument = argument.is_multi or argument.is_greedy self._arguments.append(argument) return self @typing.overload def add_option( self: _ShlexParserT, key: str, name: str, /, *names: str, default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: ... @typing.overload def add_option( self: _ShlexParserT, key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[_CmpProtoT]], default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: ... @typing.overload def add_option( self: _ShlexParserT, key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[_T]], default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: ... @typing.overload def add_option( self: _ShlexParserT, key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[typing.Any]], default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: ... # TODO: add default getter def add_option( self: _ShlexParserT, key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: # <<inherited docstring from AbstractOptionParser>>. option = Option( key, name, *names, converters=converters, default=default, empty_value=empty_value, max_value=max_value, min_value=min_value, multi=multi, ) if self._client: option.bind_client(self._client) if self._component: option.bind_component(self._component) self._options.append(option) return self def bind_client(self: _ShlexParserT, client: tanjun_abc.Client, /) -> _ShlexParserT: # <<inherited docstring from AbstractOptionParser>>. self._client = client for parameter in itertools.chain(self._options, self._arguments): parameter.bind_client(client) return self def bind_component(self: _ShlexParserT, component: tanjun_abc.Component, /) -> _ShlexParserT: # <<inherited docstring from AbstractOptionParser>>. self._component = component for parameter in itertools.chain(self._options, self._arguments): parameter.bind_component(component) return self def parse( self, ctx: tanjun_abc.MessageContext, / ) -> collections.Coroutine[typing.Any, typing.Any, dict[str, typing.Any]]: # <<inherited docstring from AbstractOptionParser>>. return _SemanticShlex(ctx, self._arguments, self._options).parse() def with_parser(command: _CommandT, /) -> _CommandT: """Add a shlex parser command parser to a supported command. Example ------- ```py @tanjun.with_argument("arg", converters=int) @tanjun.with_parser @tanjun.as_message_command("hi") async def hi(ctx: tanjun.MessageContext, arg: int) -> None: ... ``` Parameters ---------- command : tanjun.abc.MessageCommands The message command to set the parser on. Returns ------- tanjun.abc.MessageCommand The command with the parser set. Raises ------ ValueError If the command already has a parser set. """ if command.parser: raise ValueError("Command already has a parser set") return command.set_parser(ShlexParser())
Standard implementation of message command argument parsing.
View Source
class ShlexParser(AbstractOptionParser): """A shlex based `AbstractOptionParser` implementation.""" __slots__ = ("_arguments", "_client", "_component", "_options") def __init__(self) -> None: """Initialise a shlex parser.""" self._arguments: list[Argument] = [] self._client: typing.Optional[tanjun_abc.Client] = None self._component: typing.Optional[tanjun_abc.Component] = None self._options: list[Option] = [] # TODO: maybe switch to dict[str, Option] and assert doesn't already exist @property def needs_injector(self) -> bool: """Whether this parser needs an injector to be used.""" # TODO: cache this value? return any(parameter.needs_injector for parameter in itertools.chain(self._options, self._arguments)) @property def arguments(self) -> collections.Sequence[Argument]: # <<inherited docstring from AbstractOptionParser>>. return self._arguments.copy() @property def options(self) -> collections.Sequence[Option]: # <<inherited docstring from AbstractOptionParser>>. return self._options.copy() def copy(self: _ShlexParserT, *, _new: bool = True) -> _ShlexParserT: # <<inherited docstring from AbstractOptionParser>>. if not _new: self._arguments = [argument.copy() for argument in self._arguments] self._options = [option.copy() for option in self._options] return self return copy.copy(self).copy(_new=False) @typing.overload def add_argument( self: _ShlexParserT, key: str, /, *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: ... @typing.overload def add_argument( self: _ShlexParserT, key: str, /, converters: _MaybeIterable[ConverterSig[_CmpProtoT]], *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: ... @typing.overload def add_argument( self: _ShlexParserT, key: str, /, converters: _MaybeIterable[ConverterSig[_T]], *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: ... @typing.overload def add_argument( self: _ShlexParserT, key: str, /, converters: _MaybeIterable[ConverterSig[typing.Any]], *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, multi: bool = False, ) -> _ShlexParserT: ... def add_argument( self: _ShlexParserT, key: str, /, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: # <<inherited docstring from AbstractOptionParser>>. argument = Argument( key, converters=converters, default=default, greedy=greedy, max_value=max_value, min_value=min_value, multi=multi, ) if self._client: argument.bind_client(self._client) if self._component: argument.bind_component(self._component) found_final_argument = False for argument in self._arguments: if found_final_argument: del self._arguments[-1] raise ValueError("Multi or greedy argument must be the last argument") found_final_argument = argument.is_multi or argument.is_greedy self._arguments.append(argument) return self @typing.overload def add_option( self: _ShlexParserT, key: str, name: str, /, *names: str, default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[str]] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: ... @typing.overload def add_option( self: _ShlexParserT, key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[_CmpProtoT]], default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, min_value: _UndefinedOr[_CmpProtoT] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: ... @typing.overload def add_option( self: _ShlexParserT, key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[_T]], default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[_T]] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: ... @typing.overload def add_option( self: _ShlexParserT, key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[typing.Any]], default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: ... # TODO: add default getter def add_option( self: _ShlexParserT, key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: # <<inherited docstring from AbstractOptionParser>>. option = Option( key, name, *names, converters=converters, default=default, empty_value=empty_value, max_value=max_value, min_value=min_value, multi=multi, ) if self._client: option.bind_client(self._client) if self._component: option.bind_component(self._component) self._options.append(option) return self def bind_client(self: _ShlexParserT, client: tanjun_abc.Client, /) -> _ShlexParserT: # <<inherited docstring from AbstractOptionParser>>. self._client = client for parameter in itertools.chain(self._options, self._arguments): parameter.bind_client(client) return self def bind_component(self: _ShlexParserT, component: tanjun_abc.Component, /) -> _ShlexParserT: # <<inherited docstring from AbstractOptionParser>>. self._component = component for parameter in itertools.chain(self._options, self._arguments): parameter.bind_component(component) return self def parse( self, ctx: tanjun_abc.MessageContext, / ) -> collections.Coroutine[typing.Any, typing.Any, dict[str, typing.Any]]: # <<inherited docstring from AbstractOptionParser>>. return _SemanticShlex(ctx, self._arguments, self._options).parse()
A shlex based AbstractOptionParser implementation.
View Source
def __init__(self) -> None: """Initialise a shlex parser.""" self._arguments: list[Argument] = [] self._client: typing.Optional[tanjun_abc.Client] = None self._component: typing.Optional[tanjun_abc.Component] = None self._options: list[Option] = [] # TODO: maybe switch to dict[str, Option] and assert doesn't already exist
Initialise a shlex parser.
Whether this parser needs an injector to be used.
Sequence of the positional arguments registered with this parser.
Sequence of the named options registered with this parser.
View Source
def copy(self: _ShlexParserT, *, _new: bool = True) -> _ShlexParserT: # <<inherited docstring from AbstractOptionParser>>. if not _new: self._arguments = [argument.copy() for argument in self._arguments] self._options = [option.copy() for option in self._options] return self return copy.copy(self).copy(_new=False)
Copy the parser.
Returns
- Self: A copy of the parser.
View Source
def add_argument( self: _ShlexParserT, key: str, /, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: # <<inherited docstring from AbstractOptionParser>>. argument = Argument( key, converters=converters, default=default, greedy=greedy, max_value=max_value, min_value=min_value, multi=multi, ) if self._client: argument.bind_client(self._client) if self._component: argument.bind_component(self._component) found_final_argument = False for argument in self._arguments: if found_final_argument: del self._arguments[-1] raise ValueError("Multi or greedy argument must be the last argument") found_final_argument = argument.is_multi or argument.is_greedy self._arguments.append(argument) return self
Add a positional argument type to the parser..
Note: Order matters for positional arguments.
Parameters
- key (str): The string identifier of this argument (may be used to pass the result of this argument to the command's callback during execution).
Other Parameters
converters (typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]): The converter(s) this argument should use to handle values passed to it during parsing.
If no converters are provided then the raw string value will be passed.
Only the first converter to pass will be used.
- default (typing.Any):
The default value of this argument, if left as
UNDEFINEDthen this will have no default. - greedy (bool): Whether or not this argument should be greedy (meaning that it takes in the remaining argument values).
- max_value: Assert that the parsed value(s) for this argument are less than or equal to this.
If any converters are provided then this should be compatible with the result of them.
- min_value: Assert that the parsed value(s) for this argument are greater than or equal to this.
If any converters are provided then this should be compatible with the result of them.
- multi (bool): Whether this argument can be passed multiple times.
Returns
- Self: This parser to enable chained calls.
View Source
def add_option( self: _ShlexParserT, key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, multi: bool = False, ) -> _ShlexParserT: # <<inherited docstring from AbstractOptionParser>>. option = Option( key, name, *names, converters=converters, default=default, empty_value=empty_value, max_value=max_value, min_value=min_value, multi=multi, ) if self._client: option.bind_client(self._client) if self._component: option.bind_component(self._component) self._options.append(option) return self
Add an named option to this parser.
Parameters
- key (str): The string identifier of this option which will be used to pass the result of this option to the command's callback during execution as a keyword argument.
- name (str): The name of this option used for identifying it in the parsed content.
- default (typing.Any): The default value of this option, unlike arguments this is required for options.
Other Parameters
- *names (str): Other names of this option used for identifying it in the parsed content.
converters (typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]): The converter(s) this option should use to handle values passed to it during parsing.
If no converters are provided then the raw string value will be passed.
Only the first converter to pass will be used.
- empty_value (typing.Any):
The value to use if this option is provided without a value.
If left as
UNDEFINEDthen this option will error if it's provided without a value. - max_value: Assert that the parsed value(s) for this option are less than or equal to this.
If any converters are provided then this should be compatible with the result of them.
- min_value: Assert that the parsed value(s) for this option are greater than or equal to this.
If any converters are provided then this should be compatible with the result of them.
- multi (bool):
If this option can be provided multiple times.
Defaults to
False.
Returns
- Self: This parser to enable chained calls.
View Source
def bind_client(self: _ShlexParserT, client: tanjun_abc.Client, /) -> _ShlexParserT: # <<inherited docstring from AbstractOptionParser>>. self._client = client for parameter in itertools.chain(self._options, self._arguments): parameter.bind_client(client) return self
View Source
def bind_component(self: _ShlexParserT, component: tanjun_abc.Component, /) -> _ShlexParserT: # <<inherited docstring from AbstractOptionParser>>. self._component = component for parameter in itertools.chain(self._options, self._arguments): parameter.bind_component(component) return self
View Source
def parse( self, ctx: tanjun_abc.MessageContext, / ) -> collections.Coroutine[typing.Any, typing.Any, dict[str, typing.Any]]: # <<inherited docstring from AbstractOptionParser>>. return _SemanticShlex(ctx, self._arguments, self._options).parse()
Parse a message context.
Warning:
This relies on the prefix and command name(s) having been removed
from tanjun.abc.MessageContext.content
Parameters
- ctx (tanjun.abc.MessageContext): The message context to parse.
Returns
- dict[str, typing.Any]: Dictionary of argument names to the parsed values for them.
Raises
- tanjun.errors.ParserError: If the message could not be parsed.
View Source
def with_argument( key: str, /, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), *, default: _UndefinedOr[typing.Any] = UNDEFINED, greedy: bool = False, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, multi: bool = False, ) -> collections.Callable[[_CommandT], _CommandT]: """Add an argument to a message command through a decorator call. Notes ----- * Order matters for positional arguments and since decorator execution starts at the decorator closest to the command and goes upwards this will decide where a positional argument is located in a command's signature. * If no parser is explicitly set on the command this is decorating before this decorator call then this will set `ShlexParser` as the parser. Parameters ---------- key : str The string identifier of this argument (may be used to pass the result of this argument to the command's callback during execution). converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]] The converter(s) this argument should use to handle values passed to it during parsing. If no converters are provided then the raw string value will be passed. Only the first converter to pass will be used. default : typing.Any The default value of this argument, if left as `UNDEFINED` then this will have no default. greedy : bool Whether or not this argument should be greedy (meaning that it takes in the remaining argument values). max_value Assert that the parsed value(s) for this argument are less than or equal to this. If any converters are provided then this should be compatible with the result of them. min_value Assert that the parsed value(s) for this argument are greater than or equal to this. If any converters are provided then this should be compatible with the result of them. multi : bool Whether this argument can be passed multiple times. Returns ------- collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]: Decorator function for the message command this argument is being added to. Examples -------- ```python import tanjun @tanjun.parsing.with_argument("command", converters=int, default=42) @tanjun.parsing.with_parser @tanjun.component.as_message_command("command") async def command(self, ctx: tanjun.abc.Context, /, argument: int): ... ``` """ def decorator(command: _CommandT, /) -> _CommandT: _get_or_set_parser(command).add_argument( key, converters=converters, default=default, greedy=greedy, max_value=max_value, min_value=min_value, multi=multi, ) return command return decorator
Add an argument to a message command through a decorator call.
Notes
- Order matters for positional arguments and since decorator execution starts at the decorator closest to the command and goes upwards this will decide where a positional argument is located in a command's signature.
- If no parser is explicitly set on the command this is decorating before
this decorator call then this will set
ShlexParseras the parser.
Parameters
- key (str): The string identifier of this argument (may be used to pass the result of this argument to the command's callback during execution).
converters (typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]): The converter(s) this argument should use to handle values passed to it during parsing.
If no converters are provided then the raw string value will be passed.
Only the first converter to pass will be used.
- default (typing.Any):
The default value of this argument, if left as
UNDEFINEDthen this will have no default. - greedy (bool): Whether or not this argument should be greedy (meaning that it takes in the remaining argument values).
- max_value: Assert that the parsed value(s) for this argument are less than or equal to this.
If any converters are provided then this should be compatible with the result of them.
- min_value: Assert that the parsed value(s) for this argument are greater than or equal to this.
If any converters are provided then this should be compatible with the result of them.
- multi (bool): Whether this argument can be passed multiple times.
Returns
- collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]:: Decorator function for the message command this argument is being added to.
Examples
import tanjun
@tanjun.parsing.with_argument("command", converters=int, default=42)
@tanjun.parsing.with_parser
@tanjun.component.as_message_command("command")
async def command(self, ctx: tanjun.abc.Context, /, argument: int):
...
View Source
def with_greedy_argument( key: str, /, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), *, default: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, ) -> collections.Callable[[_CommandT], _CommandT]: """Add a greedy argument to a message command through a decorator call. Notes ----- * A greedy argument will consume the remaining positional arguments and pass them through to the converters as one joined string while also requiring that at least one more positional argument is remaining unless a default is set. * Order matters for positional arguments and since decorator execution starts at the decorator closest to the command and goes upwards this will decide where a positional argument is located in a command's signature. * If no parser is explicitly set on the command this is decorating before this decorator call then this will set `ShlexParser` as the parser. Parameters ---------- key : str The string identifier of this argument (may be used to pass the result of this argument to the command's callback during execution). Other Parameters ---------------- converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]] The converter(s) this argument should use to handle values passed to it during parsing. If no converters are provided then the raw string value will be passed. Only the first converter to pass will be used. default : typing.Any The default value of this argument, if left as `UNDEFINED` then this will have no default. max_value Assert that the parsed value(s) for this argument are less than or equal to this. If any converters are provided then this should be compatible with the result of them. min_value Assert that the parsed value(s) for this argument are greater than or equal to this. If any converters are provided then this should be compatible with the result of them. Returns ------- collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]: Decorator function for the message command this argument is being added to. Examples -------- ```python import tanjun @tanjun.parsing.with_greedy_argument("command", converters=StringView) @tanjun.parsing.with_parser @tanjun.component.as_message_command("command") async def command(self, ctx: tanjun.abc.Context, /, argument: StringView): ... ``` """ return with_argument( key, converters=converters, default=default, greedy=True, max_value=max_value, min_value=min_value )
Add a greedy argument to a message command through a decorator call.
Notes
- A greedy argument will consume the remaining positional arguments and pass them through to the converters as one joined string while also requiring that at least one more positional argument is remaining unless a default is set.
- Order matters for positional arguments and since decorator execution starts at the decorator closest to the command and goes upwards this will decide where a positional argument is located in a command's signature.
- If no parser is explicitly set on the command this is decorating before
this decorator call then this will set
ShlexParseras the parser.
Parameters
- key (str): The string identifier of this argument (may be used to pass the result of this argument to the command's callback during execution).
Other Parameters
converters (typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]): The converter(s) this argument should use to handle values passed to it during parsing.
If no converters are provided then the raw string value will be passed.
Only the first converter to pass will be used.
- default (typing.Any):
The default value of this argument, if left as
UNDEFINEDthen this will have no default. - max_value: Assert that the parsed value(s) for this argument are less than or equal to this.
If any converters are provided then this should be compatible with the result of them.
- min_value: Assert that the parsed value(s) for this argument are greater than or equal to this.
If any converters are provided then this should be compatible with the result of them.
Returns
- collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]:: Decorator function for the message command this argument is being added to.
Examples
import tanjun
@tanjun.parsing.with_greedy_argument("command", converters=StringView)
@tanjun.parsing.with_parser
@tanjun.component.as_message_command("command")
async def command(self, ctx: tanjun.abc.Context, /, argument: StringView):
...
View Source
def with_multi_argument( key: str, /, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), *, default: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, ) -> collections.Callable[[_CommandT], _CommandT]: """Add a multi-argument to a message command through a decorator call. Notes ----- * A multi argument will consume the remaining positional arguments and pass them to the converters through multiple calls while also requiring that at least one more positional argument is remaining unless a default is set and passing through the results to the command's callback as a sequence. * Order matters for positional arguments and since decorator execution starts at the decorator closest to the command and goes upwards this will decide where a positional argument is located in a command's signature. * If no parser is explicitly set on the command this is decorating before this decorator call then this will set `ShlexParser` as the parser. Parameters ---------- key : str The string identifier of this argument (may be used to pass the result of this argument to the command's callback during execution). Other Parameters ---------------- converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]] The converter(s) this argument should use to handle values passed to it during parsing. If no converters are provided then the raw string value will be passed. Only the first converter to pass will be used. default : typing.Any The default value of this argument, if left as `UNDEFINED` then this will have no default. max_value Assert that the parsed value(s) for this argument are less than or equal to this. If any converters are provided then this should be compatible with the result of them. min_value Assert that the parsed value(s) for this argument are greater than or equal to this. If any converters are provided then this should be compatible with the result of them. Returns ------- collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]: Decorator function for the message command this argument is being added to. Examples -------- ```python import tanjun @tanjun.parsing.with_multi_argument("command", converters=int) @tanjun.parsing.with_parser @tanjun.component.as_message_command("command") async def command(self, ctx: tanjun.abc.Context, /, argument: collections.abc.Sequence[int]): ... ``` """ return with_argument( key, converters=converters, default=default, max_value=max_value, min_value=min_value, multi=True )
Add a multi-argument to a message command through a decorator call.
Notes
- A multi argument will consume the remaining positional arguments and pass them to the converters through multiple calls while also requiring that at least one more positional argument is remaining unless a default is set and passing through the results to the command's callback as a sequence.
- Order matters for positional arguments and since decorator execution starts at the decorator closest to the command and goes upwards this will decide where a positional argument is located in a command's signature.
- If no parser is explicitly set on the command this is decorating before
this decorator call then this will set
ShlexParseras the parser.
Parameters
- key (str): The string identifier of this argument (may be used to pass the result of this argument to the command's callback during execution).
Other Parameters
converters (typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]): The converter(s) this argument should use to handle values passed to it during parsing.
If no converters are provided then the raw string value will be passed.
Only the first converter to pass will be used.
- default (typing.Any):
The default value of this argument, if left as
UNDEFINEDthen this will have no default. - max_value: Assert that the parsed value(s) for this argument are less than or equal to this.
If any converters are provided then this should be compatible with the result of them.
- min_value: Assert that the parsed value(s) for this argument are greater than or equal to this.
If any converters are provided then this should be compatible with the result of them.
Returns
- collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]:: Decorator function for the message command this argument is being added to.
Examples
import tanjun
@tanjun.parsing.with_multi_argument("command", converters=int)
@tanjun.parsing.with_parser
@tanjun.component.as_message_command("command")
async def command(self, ctx: tanjun.abc.Context, /, argument: collections.abc.Sequence[int]):
...
View Source
def with_option( key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, multi: bool = False, ) -> collections.Callable[[_CommandT], _CommandT]: """Add an option to a message command through a decorator call. .. note:: If no parser is explicitly set on the command this is decorating before this decorator call then this will set `ShlexParser` as the parser. Parameters ---------- key : str The string identifier of this option which will be used to pass the result of this argument to the command's callback during execution as a keyword argument. name : str The name of this option used for identifying it in the parsed content. default : typing.Any The default value of this argument, unlike arguments this is required for options. Other Parameters ---------------- *names : str Other names of this option used for identifying it in the parsed content. converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]] The converter(s) this argument should use to handle values passed to it during parsing. If no converters are provided then the raw string value will be passed. Only the first converter to pass will be used. empty_value : typing.Any The value to use if this option is provided without a value. If left as `UNDEFINED` then this option will error if it's provided without a value. max_value Assert that the parsed value(s) for this option are less than or equal to this. If any converters are provided then this should be compatible with the result of them. min_value Assert that the parsed value(s) for this option are greater than or equal to this. If any converters are provided then this should be compatible with the result of them. multi : bool If this option can be provided multiple times. Defaults to `False`. Returns ------- collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]: Decorator function for the message command this option is being added to. Examples -------- ```python import tanjun @tanjun.parsing.with_option("command", converters=int, default=42) @tanjun.parsing.with_parser @tanjun.component.as_message_command("command") async def command(self, ctx: tanjun.abc.Context, /, argument: int): ... ``` """ def decorator(command: _CommandT, /) -> _CommandT: _get_or_set_parser(command).add_option( key, name, *names, converters=converters, default=default, empty_value=empty_value, max_value=max_value, min_value=min_value, multi=multi, ) return command return decorator
Add an option to a message command through a decorator call.
Note:
If no parser is explicitly set on the command this is decorating before
this decorator call then this will set ShlexParser as the parser.
Parameters
- key (str): The string identifier of this option which will be used to pass the result of this argument to the command's callback during execution as a keyword argument.
- name (str): The name of this option used for identifying it in the parsed content.
- default (typing.Any): The default value of this argument, unlike arguments this is required for options.
Other Parameters
- *names (str): Other names of this option used for identifying it in the parsed content.
converters (typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]): The converter(s) this argument should use to handle values passed to it during parsing.
If no converters are provided then the raw string value will be passed.
Only the first converter to pass will be used.
- empty_value (typing.Any):
The value to use if this option is provided without a value. If left as
UNDEFINEDthen this option will error if it's provided without a value. - max_value: Assert that the parsed value(s) for this option are less than or equal to this.
If any converters are provided then this should be compatible with the result of them.
- min_value: Assert that the parsed value(s) for this option are greater than or equal to this.
If any converters are provided then this should be compatible with the result of them.
- multi (bool):
If this option can be provided multiple times.
Defaults to
False.
Returns
- collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]:: Decorator function for the message command this option is being added to.
Examples
import tanjun
@tanjun.parsing.with_option("command", converters=int, default=42)
@tanjun.parsing.with_parser
@tanjun.component.as_message_command("command")
async def command(self, ctx: tanjun.abc.Context, /, argument: int):
...
View Source
def with_multi_option( key: str, name: str, /, *names: str, converters: _MaybeIterable[ConverterSig[typing.Any]] = (), default: typing.Any, empty_value: _UndefinedOr[typing.Any] = UNDEFINED, max_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, min_value: _UndefinedOr[_CmpProto[typing.Any]] = UNDEFINED, ) -> collections.Callable[[_CommandT], _CommandT]: """Add an multi-option to a command's parser through a decorator call. Notes ----- * A multi option will consume all the values provided for an option and pass them through to the converters as an array of strings while also requiring that at least one value is provided for the option unless a default is set. * If no parser is explicitly set on the command this is decorating before this decorator call then this will set `ShlexParser` as the parser. Parameters ---------- key : str The string identifier of this option which will be used to pass the result of this argument to the command's callback during execution as a keyword argument. name : str The name of this option used for identifying it in the parsed content. default : typing.Any The default value of this argument, unlike arguments this is required for options. Other Parameters ---------------- *names : str Other names of this option used for identifying it in the parsed content. converters : typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]] The converter(s) this argument should use to handle values passed to it during parsing. If no converters are provided then the raw string value will be passed. Only the first converter to pass will be used. empty_value : typing.Any The value to use if this option is provided without a value. If left as `UNDEFINED` then this option will error if it's provided without a value. max_value Assert that the parsed value(s) for this option are less than or equal to this. If any converters are provided then this should be compatible with the result of them. min_value Assert that the parsed value(s) for this option are greater than or equal to this. If any converters are provided then this should be compatible with the result of them. Returns ------- collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]: Decorator function for the message command this option is being added to. Examples -------- ```python import tanjun @tanjun.parsing.with_multi_option("command", converters=int, default=()) @tanjun.parsing.with_parser @tanjun.component.as_message_command("command") async def command(self, ctx: tanjun.abc.Context, /, argument: collections.abc.Sequence[int]): ... ``` """ return with_option( key, name, *names, converters=converters, default=default, empty_value=empty_value, max_value=max_value, min_value=min_value, multi=True, )
Add an multi-option to a command's parser through a decorator call.
Notes
- A multi option will consume all the values provided for an option and pass them through to the converters as an array of strings while also requiring that at least one value is provided for the option unless a default is set.
- If no parser is explicitly set on the command this is decorating before
this decorator call then this will set
ShlexParseras the parser.
Parameters
- key (str): The string identifier of this option which will be used to pass the result of this argument to the command's callback during execution as a keyword argument.
- name (str): The name of this option used for identifying it in the parsed content.
- default (typing.Any): The default value of this argument, unlike arguments this is required for options.
Other Parameters
- *names (str): Other names of this option used for identifying it in the parsed content.
converters (typing.Union[ConverterSig, collections.abc.Iterable[ConverterSig]]): The converter(s) this argument should use to handle values passed to it during parsing.
If no converters are provided then the raw string value will be passed.
Only the first converter to pass will be used.
- empty_value (typing.Any):
The value to use if this option is provided without a value. If left as
UNDEFINEDthen this option will error if it's provided without a value. - max_value: Assert that the parsed value(s) for this option are less than or equal to this.
If any converters are provided then this should be compatible with the result of them.
- min_value: Assert that the parsed value(s) for this option are greater than or equal to this.
If any converters are provided then this should be compatible with the result of them.
Returns
- collections.abc.Callable[[tanjun.abc.MessageCommand], tanjun.abc.MessageCommand]:: Decorator function for the message command this option is being added to.
Examples
import tanjun
@tanjun.parsing.with_multi_option("command", converters=int, default=())
@tanjun.parsing.with_parser
@tanjun.component.as_message_command("command")
async def command(self, ctx: tanjun.abc.Context, /, argument: collections.abc.Sequence[int]):
...
View Source
def with_parser(command: _CommandT, /) -> _CommandT: """Add a shlex parser command parser to a supported command. Example ------- ```py @tanjun.with_argument("arg", converters=int) @tanjun.with_parser @tanjun.as_message_command("hi") async def hi(ctx: tanjun.MessageContext, arg: int) -> None: ... ``` Parameters ---------- command : tanjun.abc.MessageCommands The message command to set the parser on. Returns ------- tanjun.abc.MessageCommand The command with the parser set. Raises ------ ValueError If the command already has a parser set. """ if command.parser: raise ValueError("Command already has a parser set") return command.set_parser(ShlexParser())
Add a shlex parser command parser to a supported command.
Example
@tanjun.with_argument("arg", converters=int)
@tanjun.with_parser
@tanjun.as_message_command("hi")
async def hi(ctx: tanjun.MessageContext, arg: int) -> None:
...
Parameters
- command (tanjun.abc.MessageCommands): The message command to set the parser on.
Returns
- tanjun.abc.MessageCommand: The command with the parser set.
Raises
- ValueError: If the command already has a parser set.
View Source
# -*- coding: utf-8 -*- # cython: language_level=3 # BSD 3-Clause License # # Copyright (c) 2020-2022, Faster Speeding # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Collection of utility functions used within Tanjun.""" from __future__ import annotations __all__: list[str] = [ "gather_checks", "ALL_PERMISSIONS", "CastedView", "DM_PERMISSIONS", "calculate_everyone_permissions", "calculate_permissions", "fetch_everyone_permissions", "fetch_permissions", "match_prefix_names", ] import asyncio import typing from collections import abc as collections import hikari from . import errors from . import injecting from .dependencies import async_cache if typing.TYPE_CHECKING: from . import abc from . import checks _KeyT = typing.TypeVar("_KeyT") _ValueT = typing.TypeVar("_ValueT") _OtherValueT = typing.TypeVar("_OtherValueT") async def gather_checks(ctx: abc.Context, checks_: collections.Iterable[checks.InjectableCheck], /) -> bool: """Gather a collection of checks. Parameters ---------- ctx : tanjun.abc.Context The context to check. checks : collections.abc.Iterable[tanjun.injecting.InjectableCheck] An iterable of injectable checks. Returns ------- bool Whether all the checks passed or not. """ try: await asyncio.gather(*(check(ctx) for check in checks_)) # InjectableCheck will raise FailedCheck if a false is received so if # we get this far then it's True. return True except errors.FailedCheck: return False def match_prefix_names(content: str, names: collections.Iterable[str], /) -> typing.Optional[str]: """Search for a matching name in a string. Parameters ---------- content : str The string to match names against. names : collections.abc.Iterable[str] The names to search for. Returns ------- typing.Optional[str] The name that matched or None if no name matched. """ for name in names: # Here we enforce that a name must either be at the end of content or be followed by a space. This helps # avoid issues with ambiguous naming where a command with the names "name" and "names" may sometimes hit # the former before the latter when triggered with the latter, leading to the command potentially being # inconsistently parsed. if content == name or content.startswith(name) and content[len(name)] == " ": return name ALL_PERMISSIONS: typing.Final[hikari.Permissions] = hikari.Permissions.all_permissions() """All of all the known permissions based on the linked version of Hikari.""" DM_PERMISSIONS: typing.Final[hikari.Permissions] = ( hikari.Permissions.ADD_REACTIONS | hikari.Permissions.VIEW_CHANNEL | hikari.Permissions.SEND_MESSAGES | hikari.Permissions.EMBED_LINKS | hikari.Permissions.ATTACH_FILES | hikari.Permissions.READ_MESSAGE_HISTORY | hikari.Permissions.USE_EXTERNAL_EMOJIS | hikari.Permissions.USE_EXTERNAL_STICKERS | hikari.Permissions.USE_APPLICATION_COMMANDS ) """Bitfield of the permissions which are accessibly within DM channels.""" def _calculate_channel_overwrites( channel: hikari.GuildChannel, member: hikari.Member, permissions: hikari.Permissions ) -> hikari.Permissions: if everyone_overwrite := channel.permission_overwrites.get(member.guild_id): permissions &= ~everyone_overwrite.deny permissions |= everyone_overwrite.allow deny = hikari.Permissions.NONE allow = hikari.Permissions.NONE for overwrite in filter(None, map(channel.permission_overwrites.get, member.role_ids)): deny |= overwrite.deny allow |= overwrite.allow permissions &= ~deny permissions |= allow if member_overwrite := channel.permission_overwrites.get(member.user.id): permissions &= ~member_overwrite.deny permissions |= member_overwrite.allow return permissions def _calculate_role_permissions( roles: collections.Mapping[hikari.Snowflake, hikari.Role], member: hikari.Member ) -> hikari.Permissions: permissions = roles[member.guild_id].permissions for role in map(roles.get, member.role_ids): if role and role.id != member.guild_id: permissions |= role.permissions return permissions # TODO: implicitly handle more special cases? def calculate_permissions( member: hikari.Member, guild: hikari.Guild, roles: collections.Mapping[hikari.Snowflake, hikari.Role], *, channel: typing.Optional[hikari.GuildChannel] = None, ) -> hikari.Permissions: """Calculate the permissions a member has within a guild. Parameters ---------- member : hikari.guilds.Member Object of the member to calculate the permissions for. guild : hikari.guilds.Guild Object of the guild to calculate their permissions within. roles : collections.abc.Mapping[hikari.snowflakes.Snowflake, hikari.guilds.Role] Mapping of snowflake IDs to objects of the roles within the target guild. Other Parameters ---------------- channel : typing.Optional[hikari.channels.GuildChannel] Object of the channel to calculate the member's permissions in. If this is left as `None` then this will just calculate their permissions on a guild level. Returns ------- hikari.permissions.Permission Value of the member's permissions either within the guild or specified guild channel. """ if member.guild_id != guild.id: raise ValueError("Member object isn't from the provided guild") # Guild owners are implicitly admins. if guild.owner_id == member.user.id: return ALL_PERMISSIONS # Admin permission overrides all overwrites and is only applicable to roles. if (permissions := _calculate_role_permissions(roles, member)) & permissions.ADMINISTRATOR: return ALL_PERMISSIONS if not channel: return permissions return _calculate_channel_overwrites(channel, member, permissions) async def _fetch_channel( client: abc.Client, channel: hikari.SnowflakeishOr[hikari.PartialChannel] ) -> hikari.GuildChannel: # TODO: upgrade injecting stuff to the standard interface assert isinstance(client, injecting.InjectorClient) if isinstance(channel, hikari.GuildChannel): return channel channel_id = hikari.Snowflake(channel) if client.cache and (found_channel_ := client.cache.get_guild_channel(channel_id)): return found_channel_ if channel_cache := client.get_type_dependency(_ChannelCacheT): try: return await channel_cache.get(channel_id) except async_cache.EntryNotFound: raise except async_cache.CacheMissError: pass found_channel = await client.rest.fetch_channel(channel_id) assert isinstance(found_channel, hikari.GuildChannel), "Cannot perform operation on a DM channel." return found_channel _ChannelCacheT = async_cache.SfCache[hikari.GuildChannel] _GuildCacheT = async_cache.SfCache[hikari.Guild] _RoleCacheT = async_cache.SfCache[hikari.Role] _GuldRoleCacheT = async_cache.SfGuildBound[hikari.Role] async def fetch_permissions( client: abc.Client, member: hikari.Member, /, *, channel: typing.Optional[hikari.SnowflakeishOr[hikari.PartialChannel]] = None, ) -> hikari.Permissions: """Calculate the permissions a member has within a guild. .. note:: This callback will fallback to REST requests if cache lookups fail or are not possible. Parameters ---------- client : tanjun.abc.Client The Tanjun client to use for lookups. member : hikari.guilds.Member The object of the member to calculate the permissions for. Other Parameters ---------------- channel : typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.channels.GuildChannel]] The object of ID of the channel to get their permissions in. If left as `None` then this will return their base guild permissions. Returns ------- hikari.permissions.Permissions The calculated permissions. """ # TODO: upgrade injecting stuff to the standard interface assert isinstance(client, injecting.InjectorClient) # The ordering of how this adds and removes permissions does matter. # For more information see https://discord.com/developers/docs/topics/permissions#permission-hierarchy. guild: typing.Optional[hikari.Guild] roles: typing.Optional[collections.Mapping[hikari.Snowflake, hikari.Role]] = None guild = client.cache.get_guild(member.guild_id) if client.cache else None if not guild and (guild_cache := client.get_type_dependency(_GuildCacheT)): try: guild = await guild_cache.get(member.guild_id) except async_cache.EntryNotFound: raise except async_cache.CacheMissError: pass if not guild: guild = await client.rest.fetch_guild(member.guild_id) roles = guild.roles # Guild owners are implicitly admins. if guild.owner_id == member.user.id: return ALL_PERMISSIONS roles = roles or client.cache and client.cache.get_roles_view_for_guild(member.guild_id) if not roles and (role_cache := client.get_type_dependency(_GuldRoleCacheT)): roles = {role.id: role for role in await role_cache.iter_for_guild(member.guild_id)} if not roles: raw_roles = await client.rest.fetch_roles(member.guild_id) roles = {role.id: role for role in raw_roles} # Admin permission overrides all overwrites and is only applicable to roles. if (permissions := _calculate_role_permissions(roles, member)) & permissions.ADMINISTRATOR: return ALL_PERMISSIONS if not channel: return permissions channel = await _fetch_channel(client, channel) if channel.guild_id != guild.id: raise ValueError("Channel doesn't match up with the member's guild") return _calculate_channel_overwrites(channel, member, permissions) def calculate_everyone_permissions( everyone_role: hikari.Role, /, *, channel: typing.Optional[hikari.GuildChannel] = None, ) -> hikari.Permissions: """Calculate a guild's default permissions within the guild or for a specific channel. Parameters ---------- everyone_role : hikari.guilds.Role The guild's default @everyone role. Other Parameters ---------------- channel : typing.Optional[hikari.channels.GuildChannel] The channel to calculate the permissions for. If this is left as `None` then this will just calculate the default permissions on a guild level. Returns ------- hikari.permissions.Permissions The calculated permissions. """ # The ordering of how this adds and removes permissions does matter. # For more information see https://discord.com/developers/docs/topics/permissions#permission-hierarchy. permissions = everyone_role.permissions # Admin permission overrides all overwrites and is only applicable to roles. if permissions & permissions.ADMINISTRATOR: return ALL_PERMISSIONS if not channel: return permissions if everyone_overwrite := channel.permission_overwrites.get(everyone_role.guild_id): permissions &= ~everyone_overwrite.deny permissions |= everyone_overwrite.allow return permissions async def fetch_everyone_permissions( client: abc.Client, guild_id: hikari.Snowflake, /, *, channel: typing.Optional[hikari.SnowflakeishOr[hikari.PartialChannel]] = None, ) -> hikari.Permissions: """Calculate the permissions a guild's default @everyone role has within a guild or for a specific channel. .. note:: This callback will fallback to REST requests if cache lookups fail or are not possible. Parameters ---------- client : tanjun.abc.Client The Tanjun client to use for lookups. guild_id : hikari.snowflakes.Snowflake ID of the guild to calculate the default permissions for. Other Parameters ---------------- channel : typing.Optional[hikari.snowflakes.SnowflakeishOr[hikari.channels.PartialChannel]] The channel to calculate the permissions for. If this is left as `None` then this will just calculate the default permissions on a guild level. Returns ------- hikari.permissions.Permissions The calculated permissions. """ # TODO: upgrade injecting stuff to the standard interface assert isinstance(client, injecting.InjectorClient) # The ordering of how this adds and removes permissions does matter. # For more information see https://discord.com/developers/docs/topics/permissions#permission-hierarchy. role = client.cache.get_role(guild_id) if client.cache else None if not role and (role_cache := client.get_type_dependency(_RoleCacheT)): try: role = await role_cache.get(guild_id) except async_cache.EntryNotFound: raise except async_cache.CacheMissError: pass if not role: for role in await client.rest.fetch_roles(guild_id): if role.id == guild_id: break else: raise RuntimeError("Failed to find guild's @everyone role") permissions = role.permissions # Admin permission overrides all overwrites and is only applicable to roles. if permissions & permissions.ADMINISTRATOR: return ALL_PERMISSIONS if not channel: return permissions channel = await _fetch_channel(client, channel) if everyone_overwrite := channel.permission_overwrites.get(guild_id): permissions &= ~everyone_overwrite.deny permissions |= everyone_overwrite.allow return permissions class CastedView(collections.Mapping[_KeyT, _OtherValueT], typing.Generic[_KeyT, _ValueT, _OtherValueT]): __slots__ = ("_buffer", "_cast", "_raw_data") def __init__(self, raw_data: dict[_KeyT, _ValueT], cast: collections.Callable[[_ValueT], _OtherValueT]) -> None: self._buffer: dict[_KeyT, _OtherValueT] = {} self._cast = cast self._raw_data = raw_data def __getitem__(self, key: _KeyT, /) -> _OtherValueT: try: return self._buffer[key] except KeyError: pass entry = self._raw_data[key] result = self._cast(entry) self._buffer[key] = result return result def __iter__(self) -> collections.Iterator[_KeyT]: return iter(self._raw_data) def __len__(self) -> int: return len(self._raw_data)
Collection of utility functions used within Tanjun.
View Source
# -*- coding: utf-8 -*- # cython: language_level=3 # BSD 3-Clause License # # Copyright (c) 2020-2022, Faster Speeding # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Interface and interval implementation for a Tanjun based callback scheduler.""" from __future__ import annotations __all__: list[str] = ["AbstractSchedule", "as_interval", "IntervalSchedule"] import abc import asyncio import copy import datetime import typing from . import components from . import injecting if typing.TYPE_CHECKING: from collections import abc as collections from . import abc as tanjun_abc _CallbackSig = collections.Callable[..., collections.Awaitable[None]] _OtherCallbackT = typing.TypeVar("_OtherCallbackT", bound="_CallbackSig") _IntervalScheduleT = typing.TypeVar("_IntervalScheduleT", bound="IntervalSchedule[typing.Any]") _T = typing.TypeVar("_T") _CallbackSigT = typing.TypeVar("_CallbackSigT", bound="_CallbackSig") class AbstractSchedule(abc.ABC): """Abstract callback schedule class.""" __slots__ = () @property @abc.abstractmethod def callback(self) -> _CallbackSig: """Return the callback attached to the schedule. This will be an asynchronous function which takes zero positional arguments, returns `None` and may be relying on dependency injection. """ @property @abc.abstractmethod def is_alive(self) -> bool: """Whether the schedule is alive.""" @property @abc.abstractmethod def iteration_count(self) -> int: """Return the number of times this schedule has run. This increments after a call regardless of if it failed. """ @abc.abstractmethod def copy(self: _T) -> _T: """Copy the schedule. Returns ------- Self The copied schedule. Raises ------ RuntimeError If the schedule is active. """ @abc.abstractmethod def start( self, client: injecting.InjectorClient, /, *, loop: typing.Optional[asyncio.AbstractEventLoop] = None ) -> None: """Start the schedule. Parameters ---------- tanjun.injecting.InjectorClient The injector client calls should be resolved with. Other Parameters ---------------- loop : typing.Optional[asyncio.AbstractEventLoop] The event loop to use. If not provided, the current event loop will be used. Raises ------ RuntimeError If the scheduled callback is already running. If the current or provided event loop isn't running. """ @abc.abstractmethod def stop(self) -> None: """Stop the schedule. Raises ------ RuntimeError If the scheduled callback isn't running. """ @typing.runtime_checkable class _ComponentProto(typing.Protocol): def add_schedule(self, schedule: AbstractSchedule, /) -> typing.Any: raise NotImplementedError def as_interval( interval: typing.Union[int, float, datetime.timedelta], /, *, fatal_exceptions: collections.Sequence[type[Exception]] = (), ignored_exceptions: collections.Sequence[type[Exception]] = (), max_runs: typing.Optional[int] = None, ) -> collections.Callable[[_CallbackSigT], IntervalSchedule[_CallbackSigT]]: """Decorator to create an schedule. Parameters ---------- interval : typing.Union[int, float, datetime.timedelta] The callback for the schedule. This should be an asynchronous function which takes no positional arguments, returns `None` and may use dependency injection. Other Parameters ---------------- fatal_exceptions : collections.abc.Sequence[type[Exception]] A sequence of exceptions that will cause the schedule to stop if raised by the callback, start callback or stop callback. Defaults to no exceptions. ignored_exceptions : collections.abc.Sequence[type[Exception]] A sequence of exceptions that should be ignored if raised by the callback, start callback or stop callback. Defaults to no exceptions. max_runs : typing.Optional[int] The maximum amount of times the schedule runs. Defaults to no maximum. Returns ------- collections.Callable[[_CallbackSigT], tanjun.scheduling.IntervalSchedule[_CallbackSigT]] The decorator used to create the schedule. """ return lambda callback: IntervalSchedule( callback, interval, fatal_exceptions=fatal_exceptions, ignored_exceptions=ignored_exceptions, max_runs=max_runs, ) class IntervalSchedule(typing.Generic[_CallbackSigT], components.AbstractComponentLoader, AbstractSchedule): """A callback schedule with an interval between calls.""" __slots__ = ( "_callback", "_fatal_exceptions", "_ignored_exceptions", "_interval", "_iteration_count", "_max_runs", "_stop_callback", "_start_callback", "_task", ) def __init__( self, callback: _CallbackSigT, interval: typing.Union[datetime.timedelta, int, float], /, *, fatal_exceptions: collections.Sequence[type[Exception]] = (), ignored_exceptions: collections.Sequence[type[Exception]] = (), max_runs: typing.Optional[int] = None, ) -> None: """Initialise an interval schedule. Parameters ---------- callback : collections.abc.Callable[..., collections.abc.Awaitable[None]] The callback for the schedule. This should be an asynchronous function which takes no positional arguments, returns `None` and may use dependency injection. interval : typing.Union[datetime.timedelta, int, float] The interval between calls. Passed as a timedelta, or a number of seconds. Other Parameters ---------------- fatal_exceptions : collections.abc.Sequence[type[Exception]] A sequence of exceptions that will cause the schedule to stop if raised by the callback, start callback or stop callback. Defaults to no exceptions. ignored_exceptions : collections.abc.Sequence[type[Exception]] A sequence of exceptions that should be ignored if raised by the callback, start callback or stop callback. Defaults to no exceptions. max_runs : typing.Optional[int] The maximum amount of times the schedule runs. Defaults to no maximum. """ if isinstance(interval, datetime.timedelta): self._interval: datetime.timedelta = interval else: self._interval: datetime.timedelta = datetime.timedelta(seconds=interval) self._callback = injecting.CallbackDescriptor[None](callback) self._fatal_exceptions = tuple(fatal_exceptions) self._ignored_exceptions = tuple(ignored_exceptions) self._iteration_count: int = 0 self._max_runs = max_runs self._stop_callback: typing.Optional[injecting.CallbackDescriptor[None]] = None self._start_callback: typing.Optional[injecting.CallbackDescriptor[None]] = None self._task: typing.Optional[asyncio.Task[None]] = None @property def callback(self) -> _CallbackSigT: # <<inherited docstring from IntervalSchedule>>. return typing.cast(_CallbackSigT, self._callback.callback) @property def interval(self) -> datetime.timedelta: """The interval between scheduled callback calls.""" return self._interval @property def is_alive(self) -> bool: # <<inherited docstring from IntervalSchedule>>. return self._task is not None @property def iteration_count(self) -> int: # <<inherited docstring from IntervalSchedule>>. return self._iteration_count if typing.TYPE_CHECKING: __call__: _CallbackSigT else: async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> None: await self._callback.callback(*args, **kwargs) def copy(self: _IntervalScheduleT) -> _IntervalScheduleT: # <<inherited docstring from IntervalSchedule>>. if self._task: raise RuntimeError("Cannot copy an active schedule") return copy.copy(self) def load_into_component(self, component: tanjun_abc.Component, /) -> None: # <<inherited docstring from tanjun.components.AbstractComponentLoader>>. if isinstance(component, _ComponentProto): component.add_schedule(self) def set_start_callback(self: _IntervalScheduleT, callback: _CallbackSig, /) -> _IntervalScheduleT: """Set the callback executed before the schedule starts to run. Parameters ---------- callback : CallbackSig The callback to set. Returns ------- Self The schedule instance to enable chained calls. """ self._start_callback = injecting.CallbackDescriptor(callback) return self def set_stop_callback(self: _IntervalScheduleT, callback: _CallbackSig, /) -> _IntervalScheduleT: """Set the callback executed after the schedule is finished. Parameters ---------- callback : collections.abc.Callable[..., collections.abc.Awaitable[None]] The callback to set. Returns ------- Self The schedule instance to enable chained calls. """ self._stop_callback = injecting.CallbackDescriptor(callback) return self async def _execute(self, client: injecting.InjectorClient, /) -> None: try: await self._callback.resolve(injecting.BasicInjectionContext(client)) except self._fatal_exceptions: self.stop() raise except self._ignored_exceptions: pass async def _loop(self, client: injecting.InjectorClient, /) -> None: event_loop = asyncio.get_running_loop() try: if self._start_callback: try: await self._start_callback.resolve(injecting.BasicInjectionContext(client)) except self._ignored_exceptions: pass while not self._max_runs or self._iteration_count < self._max_runs: self._iteration_count += 1 event_loop.create_task(self._execute(client)) await asyncio.sleep(self._interval.total_seconds()) finally: self._task = None if self._stop_callback: try: await self._stop_callback.resolve(injecting.BasicInjectionContext(client)) except self._ignored_exceptions: pass def start( self, client: injecting.InjectorClient, /, *, loop: typing.Optional[asyncio.AbstractEventLoop] = None ) -> None: # <<inherited docstring from IntervalSchedule>>. if self._task: raise RuntimeError("Cannot start an active schedule") loop = loop or asyncio.get_running_loop() if not loop.is_running(): raise RuntimeError("Event loop is not running") self._task = loop.create_task(self._loop(client)) def stop(self) -> None: # <<inherited docstring from IntervalSchedule>>. if not self._task: raise RuntimeError("Schedule is not running") self._task.cancel() self._task = None def with_start_callback(self, callback: _OtherCallbackT, /) -> _OtherCallbackT: """Set the callback executed before the schedule is finished/stopped. Parameters ---------- callback : collections.abc.Callable[..., collections.abc.Awaitable[None]] The callback to set. Returns ------- collections.abc.Callable[..., collections.abc.Awaitable[None]] The added callback. Examples -------- ```py @component.with_schedule() @tanjun.as_interval(1, max_runs=20) async def interval(): global counter counter += 1 print(f"Run #{counter}") @interval.with_start_callback async def pre(): print("pre callback") ``` """ self.set_start_callback(callback) return callback def with_stop_callback(self, callback: _OtherCallbackT, /) -> _OtherCallbackT: """Set the callback executed after the schedule is finished. Parameters ---------- callback : collections.abc.Callable[..., collections.abc.Awaitable[None]] The callback to set. Returns ------- collections.abc.Callable[..., collections.abc.Awaitable[None]] The added callback. Examples -------- ```py @component.with_schedule() @tanjun.as_interval(1, max_runs=20) async def interval(): global counter counter += 1 print(f"Run #{counter}") @interval.with_stop_callback async def post(): print("pre callback") ``` """ self.set_stop_callback(callback) return callback def set_ignored_exceptions(self: _IntervalScheduleT, *exceptions: type[Exception]) -> _IntervalScheduleT: """Set the exceptions that a schedule will ignore. If any of these exceptions are encountered, there will be nothing printed to console. Parameters ---------- *exceptions : type[Exception] Types of the exceptions to ignore. Returns ------- Self The schedule object to enable chained calls. """ self._ignored_exceptions = exceptions return self def set_fatal_exceptions(self: _IntervalScheduleT, *exceptions: type[Exception]) -> _IntervalScheduleT: """Set the exceptions that will stop a schedule. If any of these exceptions are encountered, the task will stop. Parameters ---------- *exceptions : type[Exception] Types of the exceptions to stop the task on. Returns ------- Self The schedule object to enable chianed calls. """ self._fatal_exceptions = exceptions return self
Interface and interval implementation for a Tanjun based callback scheduler.
View Source
def as_interval( interval: typing.Union[int, float, datetime.timedelta], /, *, fatal_exceptions: collections.Sequence[type[Exception]] = (), ignored_exceptions: collections.Sequence[type[Exception]] = (), max_runs: typing.Optional[int] = None, ) -> collections.Callable[[_CallbackSigT], IntervalSchedule[_CallbackSigT]]: """Decorator to create an schedule. Parameters ---------- interval : typing.Union[int, float, datetime.timedelta] The callback for the schedule. This should be an asynchronous function which takes no positional arguments, returns `None` and may use dependency injection. Other Parameters ---------------- fatal_exceptions : collections.abc.Sequence[type[Exception]] A sequence of exceptions that will cause the schedule to stop if raised by the callback, start callback or stop callback. Defaults to no exceptions. ignored_exceptions : collections.abc.Sequence[type[Exception]] A sequence of exceptions that should be ignored if raised by the callback, start callback or stop callback. Defaults to no exceptions. max_runs : typing.Optional[int] The maximum amount of times the schedule runs. Defaults to no maximum. Returns ------- collections.Callable[[_CallbackSigT], tanjun.scheduling.IntervalSchedule[_CallbackSigT]] The decorator used to create the schedule. """ return lambda callback: IntervalSchedule( callback, interval, fatal_exceptions=fatal_exceptions, ignored_exceptions=ignored_exceptions, max_runs=max_runs, )
Decorator to create an schedule.
Parameters
interval (typing.Union[int, float, datetime.timedelta]): The callback for the schedule.
This should be an asynchronous function which takes no positional arguments, returns
Noneand may use dependency injection.
Other Parameters
fatal_exceptions (collections.abc.Sequence[type[Exception]]): A sequence of exceptions that will cause the schedule to stop if raised by the callback, start callback or stop callback.
Defaults to no exceptions.
ignored_exceptions (collections.abc.Sequence[type[Exception]]): A sequence of exceptions that should be ignored if raised by the callback, start callback or stop callback.
Defaults to no exceptions.
- max_runs (typing.Optional[int]): The maximum amount of times the schedule runs. Defaults to no maximum.
Returns
- collections.Callable[[_CallbackSigT], tanjun.scheduling.IntervalSchedule[_CallbackSigT]]: The decorator used to create the schedule.